sellmate-design-system-react 3.3.0 → 3.3.1
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 +109 -36
- package/README.md +26 -2
- package/dist/llms-full.txt +109 -36
- package/dist/llms.txt +109 -36
- package/dist/styles.css +3 -0
- package/eslint/lib/table-column.mjs +42 -6
- package/eslint/rules/require-locale-number.mjs +9 -7
- package/eslint/rules/table-numeric-align.mjs +17 -6
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -118,7 +118,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
118
118
|
|
|
119
119
|
콤마 없는 `39000` 은 자릿수를 세어야 읽히지만 `39,000` 은 한눈에 읽힌다. 숫자를 그대로 출력하는 코드는 미완성으로 본다.
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
**번호·코드는 제외한다.** 전화번호·사업자번호·송장번호·상품코드처럼 대상을 가리키는 값은 크기를 비교하는 숫자가 아니라 **서식이 정해진 문자열**이다. 여기에 콤마를 넣으면 송장번호 `123456789` 가 `123,456,789` 로 보여 값 자체가 달라진다.
|
|
122
122
|
|
|
123
123
|
---
|
|
124
124
|
|
|
@@ -146,6 +146,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
146
146
|
|
|
147
147
|
페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
|
|
148
148
|
|
|
149
|
+
**보조 설명의 색** — 기본은 `text-fg-tertiary`(`grey_65`) 다. 보조 설명 안에서 위계가 한 단계 더 필요할 때만 `text-fg-secondary`(`grey_80`) → `text-fg-tertiary`(`grey_65`) 순으로 내려 쓴다 (§2-3).
|
|
150
|
+
|
|
149
151
|
### 2-2. 간격 (spacing)
|
|
150
152
|
|
|
151
153
|
- 유틸리티 숫자 = px. 스케일: `2 4 6 8 10 12 16 19 20 22 24 28 32 36 40 48 60`
|
|
@@ -157,8 +159,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
157
159
|
| **페이지 콘텐츠 패딩** | **`p-20`** |
|
|
158
160
|
| **섹션 ↔ 섹션** | **`gap-12`** |
|
|
159
161
|
| **페이지 내 블록 ↔ 블록** (헤더·필터·툴바·테이블 사이) | **`gap-12`** |
|
|
160
|
-
| 요소 ↔ 요소
|
|
161
|
-
| 제목 ↔ 설명 텍스트 | `gap-4
|
|
162
|
+
| 요소 ↔ 요소 | **기본 `gap-8`** (`--sys-space-stack-gap-element-normal`) · 타이트 `gap-4`(`-tight`) · 여유 `gap-12`(`-relaxed`) / `gap-16`(`-wide`) |
|
|
163
|
+
| 제목 ↔ 설명 텍스트 | **수직 배치 `gap-4`**(`--sys-space-stack-gap-text-normal`, 타이트 `gap-2`) · **가로 배치 `gap-8`**(`-relaxed`) |
|
|
162
164
|
|
|
163
165
|
정보 밀도가 높은 서비스라 블록 간격을 넓게 벌리지 않는다. `gap-16` / `gap-24` 를 페이지 골격의 기본값으로 쓰지 않는다.
|
|
164
166
|
|
|
@@ -198,23 +200,28 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
198
200
|
|
|
199
201
|
**시맨틱 유틸리티를 우선 사용한다** — 의미가 이름에 담긴 토큰이 이미 유틸리티로 존재한다.
|
|
200
202
|
|
|
201
|
-
#### 텍스트
|
|
203
|
+
#### 텍스트 회색 위계는 세 단계다
|
|
202
204
|
|
|
203
|
-
|
|
|
204
|
-
| --- | --- | --- |
|
|
205
|
-
|
|
|
206
|
-
| **보조
|
|
205
|
+
| 단계 | 유틸리티 | 값 | 용도 |
|
|
206
|
+
| --- | --- | --- | --- |
|
|
207
|
+
| **기본** | 지정하지 않는다 — 전역 기본색이 이미 적용된다 (`text-fg-primary`) | `grey_95` `#222222` | 본문·제목 |
|
|
208
|
+
| **보조 1** | **`text-fg-secondary`** | `grey_80` `#555555` | 보조 정보 |
|
|
209
|
+
| **보조 2 / 비활성** | **`text-fg-tertiary`** | `grey_65` `#888888` | 부가 설명, 비활성 |
|
|
207
210
|
|
|
211
|
+
- **위계는 순차 적용한다.** 기본 → 보조1 → 보조2 순으로 내려가며 중간 단계를 건너뛰지 않는다. 본문 바로 아래에 곧장 `text-fg-tertiary` 를 쓰지 않는다.
|
|
208
212
|
- `theme.css` 가 `body` 에 `grey_95` 를 깔아두므로 **본문에 텍스트 색 클래스를 붙이지 않는다.** `text-fg-primary` 를 매번 쓰는 것은 불필요하다.
|
|
209
|
-
-
|
|
210
|
-
-
|
|
213
|
+
- 위계가 한 단계면 충분한 보조 설명은 `text-fg-tertiary` 하나로 끝낸다 (§2-1). 두 단계가 필요할 때만 `text-fg-secondary` 를 끼워 넣는다.
|
|
214
|
+
- 비활성 표시는 단계와 무관하게 `text-fg-tertiary` 다.
|
|
211
215
|
|
|
212
216
|
```tsx
|
|
213
217
|
✅ <p>주문이 접수되었습니다.</p> // 색 지정 없음 = grey_95
|
|
214
|
-
✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명
|
|
218
|
+
✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명 한 단계
|
|
219
|
+
✅ <> // 보조 설명 두 단계
|
|
220
|
+
<p className="text-fg-secondary">배송비 정책</p>
|
|
221
|
+
<p className="text-fg-tertiary">3만원 이상 무료</p>
|
|
222
|
+
</>
|
|
215
223
|
✅ <span className="text-fg-tertiary">-</span> // 빈 값 (§3-4)
|
|
216
224
|
❌ <p className="text-fg-primary">주문이 접수되었습니다.</p> // 불필요
|
|
217
|
-
❌ <p className="text-fg-secondary">최근 30일 기준</p> // 중간 톤 사용
|
|
218
225
|
```
|
|
219
226
|
|
|
220
227
|
#### 그 밖의 색
|
|
@@ -283,7 +290,7 @@ function OrderModal({ orderId, open, onOpenChange, onClose, modalRef }: OrderMod
|
|
|
283
290
|
width={720}
|
|
284
291
|
// 주 액션은 button(단수), 보조 버튼은 footerLeft — 하단 버튼 양끝 분리 규칙과 같다
|
|
285
292
|
button={{ label: '접수', onClick: () => modalRef.ok() }}
|
|
286
|
-
footerLeft={<SButton color="
|
|
293
|
+
footerLeft={<SButton color="neutral" outline label="취소" onClick={() => modalRef.cancel()} />}
|
|
287
294
|
>
|
|
288
295
|
<SKeyValueTable fields={orderFields} values={order} />
|
|
289
296
|
</SActionModal>
|
|
@@ -298,19 +305,26 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
|
|
|
298
305
|
|
|
299
306
|
### 3-4. 테이블 컬럼 정렬
|
|
300
307
|
|
|
301
|
-
|
|
308
|
+
**값의 크기를 비교하는 숫자 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
|
|
302
309
|
자릿수가 세로로 맞아야 값의 크기를 눈으로 비교할 수 있기 때문이다.
|
|
303
310
|
|
|
304
|
-
|
|
311
|
+
**판별 기준은 "숫자인가"가 아니라 "크기를 비교하는가"다.** 자릿수 차이가 거의 없고 값끼리 대소를 견줄 일이 없으면 숫자로만 이루어져 있어도 우측 정렬하지 않는다.
|
|
312
|
+
|
|
313
|
+
| 값 성격 | 정렬 | 예 |
|
|
305
314
|
| --- | --- | --- |
|
|
306
|
-
| **금액·수량·개수·비율 등
|
|
307
|
-
|
|
|
315
|
+
| **금액·수량·개수·비율 등 양을 나타내는 값** | **`'right'`** | `39,000원` · `12개` · `3건` · `15%` |
|
|
316
|
+
| 코드·식별자 (주문번호, 상품코드, 순번) | **`'center'`** | `RV20250728-000010` · `1024` |
|
|
317
|
+
| 전화번호·사업자번호 | **`'center'`** | `010-1234-5678` |
|
|
318
|
+
| 일자·일시 | **`'center'`** | `2024-10-23` |
|
|
308
319
|
| 텍스트 | 생략(기본 `left`) | 상품명, 카테고리 |
|
|
309
320
|
| 상태 태그·아이콘·체크박스 등 고정폭 요소 | `'center'` | `STag`, `SIcon` |
|
|
310
321
|
|
|
322
|
+
**중앙 정렬은 `align: 'center'` 를 명시한다.** 기본값이 좌측이라 생략하면 중앙이 되지 않는다.
|
|
323
|
+
|
|
311
324
|
```tsx
|
|
312
325
|
const columns: STableColumn[] = [
|
|
313
|
-
{ name: '
|
|
326
|
+
{ name: 'orderNo', label: '주문번호', field: 'orderNo', width: '140px', align: 'center' },
|
|
327
|
+
{ name: 'orderedAt', label: '주문일자', field: 'orderedAt', width: '100px', align: 'center' },
|
|
314
328
|
{ name: 'name', label: '상품명', field: 'name' }, // 텍스트 → 생략
|
|
315
329
|
{ name: 'qty', label: '수량', field: 'qty', width: '80px', align: 'right',
|
|
316
330
|
format: (v: number) => `${Number(v).toLocaleString()}개` },
|
|
@@ -321,8 +335,16 @@ const columns: STableColumn[] = [
|
|
|
321
335
|
];
|
|
322
336
|
```
|
|
323
337
|
|
|
324
|
-
- `format` 으로 단위를 붙이더라도
|
|
325
|
-
- 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
|
|
338
|
+
- `format` 으로 단위를 붙이더라도 **양을 나타내면 오른쪽 정렬**이다. 단위 때문에 문자열이 되는 것은 정렬 판단과 무관하다.
|
|
339
|
+
- 양을 나타내는 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
|
|
340
|
+
- **번호·코드에는 세 자리 콤마를 넣지 않는다.** 송장번호 `123456789` 를 `123,456,789` 로 표시하면 값 자체가 달라 보인다.
|
|
341
|
+
- **헤더는 가운데, 셀만 우측**으로 두려면 `align` 이 아니라 `tdClass` 를 쓴다. `align` 은 `<th>` 와 `<td>` 에 함께 적용된다.
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
{ name: 'views', label: '조회수', field: 'views', align: 'center', tdClass: 'text-right!',
|
|
345
|
+
format: (v: number) => Number(v).toLocaleString() },
|
|
346
|
+
```
|
|
347
|
+
|
|
326
348
|
- `SKeyValueTable` 의 값 셀도 같은 기준을 따른다.
|
|
327
349
|
|
|
328
350
|
#### 값이 없는 셀은 회색 하이픈
|
|
@@ -350,13 +372,58 @@ const columns: STableColumn[] = [
|
|
|
350
372
|
|
|
351
373
|
| 상황 | 사용 |
|
|
352
374
|
| --- | --- |
|
|
353
|
-
| 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `size`: xs~
|
|
354
|
-
| **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** |
|
|
375
|
+
| 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `outline`, `size`: xs~md) |
|
|
376
|
+
| **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** (아래 3-5-3) |
|
|
355
377
|
| 텍스트만 있는 저강조 액션 | `SGhostButton` |
|
|
356
378
|
| 본문 속 이동 링크 | `STextLink` |
|
|
357
379
|
| 메인 액션 + 부가 메뉴 | `SDropdownButton` |
|
|
358
380
|
|
|
359
|
-
|
|
381
|
+
#### 3-5-1. 채움 버튼은 페이지당 개수 제한이 있다
|
|
382
|
+
|
|
383
|
+
`outline` 이 위계를 한 단계 낮춘다. **채움(outline 없음)이 그 페이지의 최상위 액션**이다.
|
|
384
|
+
|
|
385
|
+
| 조합 | 위계 | 페이지당 |
|
|
386
|
+
| --- | --- | --- |
|
|
387
|
+
| `color="primary"` (진남색 채움) | 최상위 실행 | **1개** |
|
|
388
|
+
| `color="danger"` (빨강 채움) | 최상위 파괴 | **1개** |
|
|
389
|
+
| `color="secondary"` (밝은 파랑 채움) | 중간 | N개 — **같은 속성 연속 배치 금지** |
|
|
390
|
+
| `primary outline` · `neutral outline` · `neutral`(흰 채움) · `danger outline` | 낮음 | N개 |
|
|
391
|
+
|
|
392
|
+
`SDropdownButton` 도 같은 규칙을 따르며, **페이지당 `primary` 채움 1개 계산에 포함**된다.
|
|
393
|
+
|
|
394
|
+
#### 3-5-2. `size` 는 놓이는 위치가 정한다
|
|
395
|
+
|
|
396
|
+
| 위치 | size |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| 테이블 **행 내부** 인라인 액션 | `xs` |
|
|
399
|
+
| 화면·목록 액션 (툴바, `STableBar`) | `sm` |
|
|
400
|
+
| **모달·위저드 푸터** | `md` |
|
|
401
|
+
|
|
402
|
+
`lg` 는 현재 사용처가 없다. 페이지 골격에서 쓰지 않는다.
|
|
403
|
+
|
|
404
|
+
#### 3-5-3. 파괴적 액션의 채움 / outline
|
|
405
|
+
|
|
406
|
+
물리적 위치가 아니라 **적용 범위 + 위험도**로 가른다.
|
|
407
|
+
|
|
408
|
+
| 상황 | 스타일 |
|
|
409
|
+
| --- | --- |
|
|
410
|
+
| 대상이 **전체**이거나 기능적 위험도가 높음 (계정 삭제, 전체 초기화) | `color="danger"` **채움** + 다른 버튼과 공간 분리 |
|
|
411
|
+
| **항목별**로 실행되거나 위험도가 상대적으로 낮음 (행 삭제, 연동 해제) | `color="danger" outline` |
|
|
412
|
+
|
|
413
|
+
#### 3-5-4. `SDropdownButton` — `split` 기준
|
|
414
|
+
|
|
415
|
+
| 설정 | 동작 | 쓰는 때 |
|
|
416
|
+
| --- | --- | --- |
|
|
417
|
+
| `split={false}` (기본) | 버튼 전체가 메뉴 토글. 클릭만으로는 실행되지 않는다 | 실행 전에 **범위·옵션을 골라야** 할 때 — `적치 지시서 출력 ▾` → 전체 항목 / 선택 항목만 |
|
|
418
|
+
| `split` | 좌측 = 기본 동작 **즉시 실행**, 우측 `⋯` = 변형 동작 | 기본 동작이 명확하고 **변형·부가 동작**을 곁들일 때 — `저장 \| 저장 후 신규 등록` |
|
|
419
|
+
|
|
420
|
+
메뉴 항목은 **기본 동작의 변형·부가 동작으로 한정**한다. 무관한 액션 여러 개를 한 드롭다운에 묶지 않는다.
|
|
421
|
+
|
|
422
|
+
#### 3-5-5. 그 밖
|
|
423
|
+
|
|
424
|
+
- 라벨은 **동사(+목적어)** — `저장` · `적치 지시` · `송장 재출력`
|
|
425
|
+
- 완료된 작업 버튼은 제거하지 않고 **`disabled` 로 남긴다.** 자리가 사라지면 행 높이가 흔들린다.
|
|
426
|
+
- **모달 보조 액션은 `color="neutral" outline`** 이 표준이다.
|
|
360
427
|
|
|
361
428
|
**하단 버튼 배치 (모든 화면 공통)** — 그룹을 모아 정렬하지 않고 **양끝으로 벌린다**(`justify-between`).
|
|
362
429
|
|
|
@@ -366,10 +433,10 @@ const columns: STableColumn[] = [
|
|
|
366
433
|
|
|
367
434
|
```tsx
|
|
368
435
|
<div className="flex items-center justify-between">
|
|
369
|
-
<SButton color="
|
|
436
|
+
<SButton color="neutral" outline label="취소" /> {/* 되돌리기 = 낮은 위계 */}
|
|
370
437
|
<div className="flex items-center gap-8">
|
|
371
438
|
<SCheckbox label="계속 등록하기" value={keep} onValueChange={setKeep} /> {/* 부가 요소 */}
|
|
372
|
-
<SButton label="저장" />
|
|
439
|
+
<SButton label="저장" /> {/* 페이지 유일한 primary 채움 */}
|
|
373
440
|
</div>
|
|
374
441
|
</div>
|
|
375
442
|
```
|
|
@@ -475,10 +542,14 @@ export default function ProductListPage() {
|
|
|
475
542
|
total={total}
|
|
476
543
|
selected={selected.length}
|
|
477
544
|
actions={
|
|
478
|
-
|
|
545
|
+
/* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
|
|
546
|
+
<SButton size="sm" color="danger" outline label="선택 삭제"
|
|
479
547
|
disabled={!selected.length} onClick={removeSelected} />
|
|
480
548
|
}
|
|
481
|
-
rightActions={
|
|
549
|
+
rightActions={
|
|
550
|
+
/* 이 페이지의 유일한 primary 채움 (§3-5-1) */
|
|
551
|
+
<SButton size="sm" label="상품 등록" onClick={goCreate} />
|
|
552
|
+
}
|
|
482
553
|
/>
|
|
483
554
|
|
|
484
555
|
<STable
|
|
@@ -548,7 +619,7 @@ export default function ProductCreatePage() {
|
|
|
548
619
|
|
|
549
620
|
{/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
|
|
550
621
|
<div className="flex items-center justify-between">
|
|
551
|
-
<SButton type="button" color="
|
|
622
|
+
<SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
|
|
552
623
|
<div className="flex items-center gap-8">
|
|
553
624
|
<SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
|
|
554
625
|
<SButton type="submit" label="저장" />
|
|
@@ -621,7 +692,7 @@ export default function ProductDetailPage() {
|
|
|
621
692
|
|
|
622
693
|
{/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
|
|
623
694
|
<div className="flex items-center justify-between">
|
|
624
|
-
<SButton color="
|
|
695
|
+
<SButton color="neutral" outline label="목록" onClick={goList} />
|
|
625
696
|
<div className="flex items-center gap-8">
|
|
626
697
|
<SButton color="danger" outline label="삭제" onClick={confirmDelete} />
|
|
627
698
|
<SButton label="수정" onClick={goEdit} />
|
|
@@ -656,7 +727,8 @@ export default function ProductDetailPage() {
|
|
|
656
727
|
- [ ] `text-[14px]`, `bg-[#...]` 같은 리터럴 임의 값이 없는가 (`var(--sys-*)` 참조는 허용)
|
|
657
728
|
- [ ] 텍스트에 `typo-*` 프리셋을 썼는가
|
|
658
729
|
- [ ] 간격이 전부 토큰 스케일 값인가 (`gap-13` ❌ → `gap-12` ✅)
|
|
659
|
-
- [ ] 본문이 12px(`typo-body-sm-default`)
|
|
730
|
+
- [ ] 본문이 12px(`typo-body-sm-default`)인가 (14px 본문 ❌)
|
|
731
|
+
- [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
|
|
660
732
|
- [ ] 페이지 패딩이 `p-20`, 블록·섹션 간격이 `gap-12` 인가 (`gap-16`/`gap-24` ❌)
|
|
661
733
|
- [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-24` 등)
|
|
662
734
|
- [ ] 페이지가 §4의 표준 골격에서 시작했는가
|
|
@@ -664,11 +736,13 @@ export default function ProductDetailPage() {
|
|
|
664
736
|
- [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
|
|
665
737
|
- [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
|
|
666
738
|
- [ ] 상태 표시에 `STag size="sm"` 을 썼는가
|
|
667
|
-
- [ ]
|
|
668
|
-
- [ ]
|
|
669
|
-
- [ ] 금액·수량 등 양을 나타내는 숫자에 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
|
|
739
|
+
- [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
|
|
740
|
+
- [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
|
|
741
|
+
- [ ] 금액·수량 등 양을 나타내는 숫자에 빠짐없이 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
|
|
670
742
|
- [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
|
|
671
|
-
- [ ]
|
|
743
|
+
- [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
|
|
744
|
+
- [ ] 버튼 `size` 가 위치 규칙과 맞는가 (행 내부 `xs` / 화면 액션 `sm` / 모달 푸터 `md`)
|
|
745
|
+
- [ ] 파괴 액션의 채움/outline 이 **적용 범위 + 위험도** 기준과 맞는가
|
|
672
746
|
- [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
|
|
673
747
|
- [ ] 상태 표시·알림·확인 다이얼로그가 §3의 선택 규칙을 따르는가
|
|
674
748
|
|
|
@@ -685,9 +759,8 @@ export default function ProductDetailPage() {
|
|
|
685
759
|
| `sellmate/no-arbitrary-class` | §1-2 토큰 있는 속성의 임의 값 (`text-[14px]`, `bg-[#eee]`) |
|
|
686
760
|
| `sellmate/no-off-scale-spacing` | §2-2 스케일 밖 간격 (`gap-13`) |
|
|
687
761
|
| `sellmate/table-numeric-align` | §3-4 숫자 컬럼의 `align: 'right'` 누락 (`--fix` 지원) |
|
|
688
|
-
| `sellmate/require-locale-number` | §1-4
|
|
762
|
+
| `sellmate/require-locale-number` | §1-4 금액·수량 등 수량 컬럼의 `toLocaleString()` 누락 |
|
|
689
763
|
| `sellmate/component-group-gap` | §2-2 컴포넌트 그룹 간격 (체크박스 가로 24 / 세로 8 등) |
|
|
690
|
-
| `sellmate/require-locale-number` | §1-4 금액·수량 등의 `toLocaleString()` 누락 |
|
|
691
764
|
| `sellmate/prefer-typo-preset` | §1-3 낱개 폰트 조합 (`text-14 font-bold`) |
|
|
692
765
|
|
|
693
766
|
`gap-13` 처럼 스케일 밖 값은 Tailwind v4 에서 **에러 없이 조용히 무시된다**. "간격이 왜 안 먹지" 로만 보이므로 반드시 스케일 값을 쓴다.
|
package/README.md
CHANGED
|
@@ -160,8 +160,8 @@ export default [
|
|
|
160
160
|
| `sellmate/no-raw-html-control` | error | 대응 컴포넌트가 있는 생 HTML (`<button>` `<input>` `<select>` `<textarea>` `<table>` `<form>` `<dialog>` `<hr>` `<details>` `<progress>`), `alert()`/`confirm()` |
|
|
161
161
|
| `sellmate/no-arbitrary-class` | error | **토큰이 있는 속성**(색·타이포·간격·모서리)의 임의 값 — `text-[14px]`, `bg-[#eee]`, `gap-[13px]`, `rounded-[5px]` |
|
|
162
162
|
| `sellmate/no-off-scale-spacing` | error | 스케일 밖 간격 (`gap-13`, `p-15`) — Tailwind v4 에서 **조용히 무시되는** 값이라 눈으로 찾기 어렵다 |
|
|
163
|
-
| `sellmate/table-numeric-align` | error |
|
|
164
|
-
| `sellmate/require-locale-number` | error |
|
|
163
|
+
| `sellmate/table-numeric-align` | error | 수량 컬럼(금액·수량 등)에 `align: 'right'` 누락 — **`--fix` 로 자동 교정** |
|
|
164
|
+
| `sellmate/require-locale-number` | error | 수량 컬럼의 `toLocaleString()` 누락 — 세 자리 콤마는 필수 |
|
|
165
165
|
| `sellmate/component-group-gap` | error | 같은 컴포넌트를 나열할 때의 그룹 간격 — 배열 방향에 따라 값이 다르다(체크박스 가로 24 / 세로 8) |
|
|
166
166
|
| `sellmate/prefer-typo-preset` | warn | `text-14 font-bold` 같은 낱개 조합 → `typo-*` 프리셋 |
|
|
167
167
|
|
|
@@ -188,6 +188,30 @@ export default [
|
|
|
188
188
|
|
|
189
189
|
핵심은 **"디자인 토큰을 하드코딩하지 마라"** 이지 "임의 값을 절대 쓰지 마라" 가 아닙니다.
|
|
190
190
|
|
|
191
|
+
테이블 두 규칙(`table-numeric-align` · `require-locale-number`)도 아래는 통과시킵니다.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// 번호·코드 등 식별자 — 크기를 비교하지 않으므로 우측 정렬도 콤마도 요구하지 않는다
|
|
195
|
+
{ name: 'waybillNo', label: '송장번호', field: 'waybillNo', align: 'center' }
|
|
196
|
+
|
|
197
|
+
// 헤더 가운데 + 셀 우측 — align 은 th·td 공통이라 tdClass 로만 표현된다
|
|
198
|
+
{ name: 'views', label: '조회수', field: 'views', align: 'center', tdClass: 'text-right!',
|
|
199
|
+
format: (v: number) => Number(v).toLocaleString() }
|
|
200
|
+
|
|
201
|
+
// render / renderCell 로 셀을 직접 그리는 컬럼 — 라벨만으로 숫자 여부를 판단하지 않는다
|
|
202
|
+
{ name: 'unitPrice', label: '단가', field: 'unitPrice', align: 'right',
|
|
203
|
+
renderCell: ({ row }) => `${row.unitPrice.toLocaleString()}원` }
|
|
204
|
+
|
|
205
|
+
// 수량 어휘를 부분 문자열로 포함하지만 값은 불리언인 상태 컬럼
|
|
206
|
+
{ name: 'isCostInput', label: '비용 입력 여부', field: 'isCostInput', align: 'center' }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
그래도 남는 예외는 컬럼 `name` 을 `allow` 로 지정해 뺍니다.
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
'sellmate/table-numeric-align': ['error', { allow: ['rank'] }],
|
|
213
|
+
```
|
|
214
|
+
|
|
191
215
|
### 더 엄격하게 / 더 느슨하게
|
|
192
216
|
|
|
193
217
|
`configs.strict` 는 전 규칙을 error 로 올리고 `<ul>` `<ol>` `<li>` `<svg>` `<label>` 까지 검사합니다.
|
package/dist/llms-full.txt
CHANGED
|
@@ -126,7 +126,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
126
126
|
|
|
127
127
|
콤마 없는 `39000` 은 자릿수를 세어야 읽히지만 `39,000` 은 한눈에 읽힌다. 숫자를 그대로 출력하는 코드는 미완성으로 본다.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
**번호·코드는 제외한다.** 전화번호·사업자번호·송장번호·상품코드처럼 대상을 가리키는 값은 크기를 비교하는 숫자가 아니라 **서식이 정해진 문자열**이다. 여기에 콤마를 넣으면 송장번호 `123456789` 가 `123,456,789` 로 보여 값 자체가 달라진다.
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
@@ -154,6 +154,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
154
154
|
|
|
155
155
|
페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
|
|
156
156
|
|
|
157
|
+
**보조 설명의 색** — 기본은 `text-fg-tertiary`(`grey_65`) 다. 보조 설명 안에서 위계가 한 단계 더 필요할 때만 `text-fg-secondary`(`grey_80`) → `text-fg-tertiary`(`grey_65`) 순으로 내려 쓴다 (§2-3).
|
|
158
|
+
|
|
157
159
|
### 2-2. 간격 (spacing)
|
|
158
160
|
|
|
159
161
|
- 유틸리티 숫자 = px. 스케일: `2 4 6 8 10 12 16 19 20 22 24 28 32 36 40 48 60`
|
|
@@ -165,8 +167,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
165
167
|
| **페이지 콘텐츠 패딩** | **`p-20`** |
|
|
166
168
|
| **섹션 ↔ 섹션** | **`gap-12`** |
|
|
167
169
|
| **페이지 내 블록 ↔ 블록** (헤더·필터·툴바·테이블 사이) | **`gap-12`** |
|
|
168
|
-
| 요소 ↔ 요소
|
|
169
|
-
| 제목 ↔ 설명 텍스트 | `gap-4
|
|
170
|
+
| 요소 ↔ 요소 | **기본 `gap-8`** (`--sys-space-stack-gap-element-normal`) · 타이트 `gap-4`(`-tight`) · 여유 `gap-12`(`-relaxed`) / `gap-16`(`-wide`) |
|
|
171
|
+
| 제목 ↔ 설명 텍스트 | **수직 배치 `gap-4`**(`--sys-space-stack-gap-text-normal`, 타이트 `gap-2`) · **가로 배치 `gap-8`**(`-relaxed`) |
|
|
170
172
|
|
|
171
173
|
정보 밀도가 높은 서비스라 블록 간격을 넓게 벌리지 않는다. `gap-16` / `gap-24` 를 페이지 골격의 기본값으로 쓰지 않는다.
|
|
172
174
|
|
|
@@ -206,23 +208,28 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
206
208
|
|
|
207
209
|
**시맨틱 유틸리티를 우선 사용한다** — 의미가 이름에 담긴 토큰이 이미 유틸리티로 존재한다.
|
|
208
210
|
|
|
209
|
-
#### 텍스트
|
|
211
|
+
#### 텍스트 회색 위계는 세 단계다
|
|
210
212
|
|
|
211
|
-
|
|
|
212
|
-
| --- | --- | --- |
|
|
213
|
-
|
|
|
214
|
-
| **보조
|
|
213
|
+
| 단계 | 유틸리티 | 값 | 용도 |
|
|
214
|
+
| --- | --- | --- | --- |
|
|
215
|
+
| **기본** | 지정하지 않는다 — 전역 기본색이 이미 적용된다 (`text-fg-primary`) | `grey_95` `#222222` | 본문·제목 |
|
|
216
|
+
| **보조 1** | **`text-fg-secondary`** | `grey_80` `#555555` | 보조 정보 |
|
|
217
|
+
| **보조 2 / 비활성** | **`text-fg-tertiary`** | `grey_65` `#888888` | 부가 설명, 비활성 |
|
|
215
218
|
|
|
219
|
+
- **위계는 순차 적용한다.** 기본 → 보조1 → 보조2 순으로 내려가며 중간 단계를 건너뛰지 않는다. 본문 바로 아래에 곧장 `text-fg-tertiary` 를 쓰지 않는다.
|
|
216
220
|
- `theme.css` 가 `body` 에 `grey_95` 를 깔아두므로 **본문에 텍스트 색 클래스를 붙이지 않는다.** `text-fg-primary` 를 매번 쓰는 것은 불필요하다.
|
|
217
|
-
-
|
|
218
|
-
-
|
|
221
|
+
- 위계가 한 단계면 충분한 보조 설명은 `text-fg-tertiary` 하나로 끝낸다 (§2-1). 두 단계가 필요할 때만 `text-fg-secondary` 를 끼워 넣는다.
|
|
222
|
+
- 비활성 표시는 단계와 무관하게 `text-fg-tertiary` 다.
|
|
219
223
|
|
|
220
224
|
```tsx
|
|
221
225
|
✅ <p>주문이 접수되었습니다.</p> // 색 지정 없음 = grey_95
|
|
222
|
-
✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명
|
|
226
|
+
✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명 한 단계
|
|
227
|
+
✅ <> // 보조 설명 두 단계
|
|
228
|
+
<p className="text-fg-secondary">배송비 정책</p>
|
|
229
|
+
<p className="text-fg-tertiary">3만원 이상 무료</p>
|
|
230
|
+
</>
|
|
223
231
|
✅ <span className="text-fg-tertiary">-</span> // 빈 값 (§3-4)
|
|
224
232
|
❌ <p className="text-fg-primary">주문이 접수되었습니다.</p> // 불필요
|
|
225
|
-
❌ <p className="text-fg-secondary">최근 30일 기준</p> // 중간 톤 사용
|
|
226
233
|
```
|
|
227
234
|
|
|
228
235
|
#### 그 밖의 색
|
|
@@ -291,7 +298,7 @@ function OrderModal({ orderId, open, onOpenChange, onClose, modalRef }: OrderMod
|
|
|
291
298
|
width={720}
|
|
292
299
|
// 주 액션은 button(단수), 보조 버튼은 footerLeft — 하단 버튼 양끝 분리 규칙과 같다
|
|
293
300
|
button={{ label: '접수', onClick: () => modalRef.ok() }}
|
|
294
|
-
footerLeft={<SButton color="
|
|
301
|
+
footerLeft={<SButton color="neutral" outline label="취소" onClick={() => modalRef.cancel()} />}
|
|
295
302
|
>
|
|
296
303
|
<SKeyValueTable fields={orderFields} values={order} />
|
|
297
304
|
</SActionModal>
|
|
@@ -306,19 +313,26 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
|
|
|
306
313
|
|
|
307
314
|
### 3-4. 테이블 컬럼 정렬
|
|
308
315
|
|
|
309
|
-
|
|
316
|
+
**값의 크기를 비교하는 숫자 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
|
|
310
317
|
자릿수가 세로로 맞아야 값의 크기를 눈으로 비교할 수 있기 때문이다.
|
|
311
318
|
|
|
312
|
-
|
|
319
|
+
**판별 기준은 "숫자인가"가 아니라 "크기를 비교하는가"다.** 자릿수 차이가 거의 없고 값끼리 대소를 견줄 일이 없으면 숫자로만 이루어져 있어도 우측 정렬하지 않는다.
|
|
320
|
+
|
|
321
|
+
| 값 성격 | 정렬 | 예 |
|
|
313
322
|
| --- | --- | --- |
|
|
314
|
-
| **금액·수량·개수·비율 등
|
|
315
|
-
|
|
|
323
|
+
| **금액·수량·개수·비율 등 양을 나타내는 값** | **`'right'`** | `39,000원` · `12개` · `3건` · `15%` |
|
|
324
|
+
| 코드·식별자 (주문번호, 상품코드, 순번) | **`'center'`** | `RV20250728-000010` · `1024` |
|
|
325
|
+
| 전화번호·사업자번호 | **`'center'`** | `010-1234-5678` |
|
|
326
|
+
| 일자·일시 | **`'center'`** | `2024-10-23` |
|
|
316
327
|
| 텍스트 | 생략(기본 `left`) | 상품명, 카테고리 |
|
|
317
328
|
| 상태 태그·아이콘·체크박스 등 고정폭 요소 | `'center'` | `STag`, `SIcon` |
|
|
318
329
|
|
|
330
|
+
**중앙 정렬은 `align: 'center'` 를 명시한다.** 기본값이 좌측이라 생략하면 중앙이 되지 않는다.
|
|
331
|
+
|
|
319
332
|
```tsx
|
|
320
333
|
const columns: STableColumn[] = [
|
|
321
|
-
{ name: '
|
|
334
|
+
{ name: 'orderNo', label: '주문번호', field: 'orderNo', width: '140px', align: 'center' },
|
|
335
|
+
{ name: 'orderedAt', label: '주문일자', field: 'orderedAt', width: '100px', align: 'center' },
|
|
322
336
|
{ name: 'name', label: '상품명', field: 'name' }, // 텍스트 → 생략
|
|
323
337
|
{ name: 'qty', label: '수량', field: 'qty', width: '80px', align: 'right',
|
|
324
338
|
format: (v: number) => `${Number(v).toLocaleString()}개` },
|
|
@@ -329,8 +343,16 @@ const columns: STableColumn[] = [
|
|
|
329
343
|
];
|
|
330
344
|
```
|
|
331
345
|
|
|
332
|
-
- `format` 으로 단위를 붙이더라도
|
|
333
|
-
- 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
|
|
346
|
+
- `format` 으로 단위를 붙이더라도 **양을 나타내면 오른쪽 정렬**이다. 단위 때문에 문자열이 되는 것은 정렬 판단과 무관하다.
|
|
347
|
+
- 양을 나타내는 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
|
|
348
|
+
- **번호·코드에는 세 자리 콤마를 넣지 않는다.** 송장번호 `123456789` 를 `123,456,789` 로 표시하면 값 자체가 달라 보인다.
|
|
349
|
+
- **헤더는 가운데, 셀만 우측**으로 두려면 `align` 이 아니라 `tdClass` 를 쓴다. `align` 은 `<th>` 와 `<td>` 에 함께 적용된다.
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
{ name: 'views', label: '조회수', field: 'views', align: 'center', tdClass: 'text-right!',
|
|
353
|
+
format: (v: number) => Number(v).toLocaleString() },
|
|
354
|
+
```
|
|
355
|
+
|
|
334
356
|
- `SKeyValueTable` 의 값 셀도 같은 기준을 따른다.
|
|
335
357
|
|
|
336
358
|
#### 값이 없는 셀은 회색 하이픈
|
|
@@ -358,13 +380,58 @@ const columns: STableColumn[] = [
|
|
|
358
380
|
|
|
359
381
|
| 상황 | 사용 |
|
|
360
382
|
| --- | --- |
|
|
361
|
-
| 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `size`: xs~
|
|
362
|
-
| **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** |
|
|
383
|
+
| 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `outline`, `size`: xs~md) |
|
|
384
|
+
| **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** (아래 3-5-3) |
|
|
363
385
|
| 텍스트만 있는 저강조 액션 | `SGhostButton` |
|
|
364
386
|
| 본문 속 이동 링크 | `STextLink` |
|
|
365
387
|
| 메인 액션 + 부가 메뉴 | `SDropdownButton` |
|
|
366
388
|
|
|
367
|
-
|
|
389
|
+
#### 3-5-1. 채움 버튼은 페이지당 개수 제한이 있다
|
|
390
|
+
|
|
391
|
+
`outline` 이 위계를 한 단계 낮춘다. **채움(outline 없음)이 그 페이지의 최상위 액션**이다.
|
|
392
|
+
|
|
393
|
+
| 조합 | 위계 | 페이지당 |
|
|
394
|
+
| --- | --- | --- |
|
|
395
|
+
| `color="primary"` (진남색 채움) | 최상위 실행 | **1개** |
|
|
396
|
+
| `color="danger"` (빨강 채움) | 최상위 파괴 | **1개** |
|
|
397
|
+
| `color="secondary"` (밝은 파랑 채움) | 중간 | N개 — **같은 속성 연속 배치 금지** |
|
|
398
|
+
| `primary outline` · `neutral outline` · `neutral`(흰 채움) · `danger outline` | 낮음 | N개 |
|
|
399
|
+
|
|
400
|
+
`SDropdownButton` 도 같은 규칙을 따르며, **페이지당 `primary` 채움 1개 계산에 포함**된다.
|
|
401
|
+
|
|
402
|
+
#### 3-5-2. `size` 는 놓이는 위치가 정한다
|
|
403
|
+
|
|
404
|
+
| 위치 | size |
|
|
405
|
+
| --- | --- |
|
|
406
|
+
| 테이블 **행 내부** 인라인 액션 | `xs` |
|
|
407
|
+
| 화면·목록 액션 (툴바, `STableBar`) | `sm` |
|
|
408
|
+
| **모달·위저드 푸터** | `md` |
|
|
409
|
+
|
|
410
|
+
`lg` 는 현재 사용처가 없다. 페이지 골격에서 쓰지 않는다.
|
|
411
|
+
|
|
412
|
+
#### 3-5-3. 파괴적 액션의 채움 / outline
|
|
413
|
+
|
|
414
|
+
물리적 위치가 아니라 **적용 범위 + 위험도**로 가른다.
|
|
415
|
+
|
|
416
|
+
| 상황 | 스타일 |
|
|
417
|
+
| --- | --- |
|
|
418
|
+
| 대상이 **전체**이거나 기능적 위험도가 높음 (계정 삭제, 전체 초기화) | `color="danger"` **채움** + 다른 버튼과 공간 분리 |
|
|
419
|
+
| **항목별**로 실행되거나 위험도가 상대적으로 낮음 (행 삭제, 연동 해제) | `color="danger" outline` |
|
|
420
|
+
|
|
421
|
+
#### 3-5-4. `SDropdownButton` — `split` 기준
|
|
422
|
+
|
|
423
|
+
| 설정 | 동작 | 쓰는 때 |
|
|
424
|
+
| --- | --- | --- |
|
|
425
|
+
| `split={false}` (기본) | 버튼 전체가 메뉴 토글. 클릭만으로는 실행되지 않는다 | 실행 전에 **범위·옵션을 골라야** 할 때 — `적치 지시서 출력 ▾` → 전체 항목 / 선택 항목만 |
|
|
426
|
+
| `split` | 좌측 = 기본 동작 **즉시 실행**, 우측 `⋯` = 변형 동작 | 기본 동작이 명확하고 **변형·부가 동작**을 곁들일 때 — `저장 \| 저장 후 신규 등록` |
|
|
427
|
+
|
|
428
|
+
메뉴 항목은 **기본 동작의 변형·부가 동작으로 한정**한다. 무관한 액션 여러 개를 한 드롭다운에 묶지 않는다.
|
|
429
|
+
|
|
430
|
+
#### 3-5-5. 그 밖
|
|
431
|
+
|
|
432
|
+
- 라벨은 **동사(+목적어)** — `저장` · `적치 지시` · `송장 재출력`
|
|
433
|
+
- 완료된 작업 버튼은 제거하지 않고 **`disabled` 로 남긴다.** 자리가 사라지면 행 높이가 흔들린다.
|
|
434
|
+
- **모달 보조 액션은 `color="neutral" outline`** 이 표준이다.
|
|
368
435
|
|
|
369
436
|
**하단 버튼 배치 (모든 화면 공통)** — 그룹을 모아 정렬하지 않고 **양끝으로 벌린다**(`justify-between`).
|
|
370
437
|
|
|
@@ -374,10 +441,10 @@ const columns: STableColumn[] = [
|
|
|
374
441
|
|
|
375
442
|
```tsx
|
|
376
443
|
<div className="flex items-center justify-between">
|
|
377
|
-
<SButton color="
|
|
444
|
+
<SButton color="neutral" outline label="취소" /> {/* 되돌리기 = 낮은 위계 */}
|
|
378
445
|
<div className="flex items-center gap-8">
|
|
379
446
|
<SCheckbox label="계속 등록하기" value={keep} onValueChange={setKeep} /> {/* 부가 요소 */}
|
|
380
|
-
<SButton label="저장" />
|
|
447
|
+
<SButton label="저장" /> {/* 페이지 유일한 primary 채움 */}
|
|
381
448
|
</div>
|
|
382
449
|
</div>
|
|
383
450
|
```
|
|
@@ -483,10 +550,14 @@ export default function ProductListPage() {
|
|
|
483
550
|
total={total}
|
|
484
551
|
selected={selected.length}
|
|
485
552
|
actions={
|
|
486
|
-
|
|
553
|
+
/* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
|
|
554
|
+
<SButton size="sm" color="danger" outline label="선택 삭제"
|
|
487
555
|
disabled={!selected.length} onClick={removeSelected} />
|
|
488
556
|
}
|
|
489
|
-
rightActions={
|
|
557
|
+
rightActions={
|
|
558
|
+
/* 이 페이지의 유일한 primary 채움 (§3-5-1) */
|
|
559
|
+
<SButton size="sm" label="상품 등록" onClick={goCreate} />
|
|
560
|
+
}
|
|
490
561
|
/>
|
|
491
562
|
|
|
492
563
|
<STable
|
|
@@ -556,7 +627,7 @@ export default function ProductCreatePage() {
|
|
|
556
627
|
|
|
557
628
|
{/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
|
|
558
629
|
<div className="flex items-center justify-between">
|
|
559
|
-
<SButton type="button" color="
|
|
630
|
+
<SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
|
|
560
631
|
<div className="flex items-center gap-8">
|
|
561
632
|
<SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
|
|
562
633
|
<SButton type="submit" label="저장" />
|
|
@@ -629,7 +700,7 @@ export default function ProductDetailPage() {
|
|
|
629
700
|
|
|
630
701
|
{/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
|
|
631
702
|
<div className="flex items-center justify-between">
|
|
632
|
-
<SButton color="
|
|
703
|
+
<SButton color="neutral" outline label="목록" onClick={goList} />
|
|
633
704
|
<div className="flex items-center gap-8">
|
|
634
705
|
<SButton color="danger" outline label="삭제" onClick={confirmDelete} />
|
|
635
706
|
<SButton label="수정" onClick={goEdit} />
|
|
@@ -664,7 +735,8 @@ export default function ProductDetailPage() {
|
|
|
664
735
|
- [ ] `text-[14px]`, `bg-[#...]` 같은 리터럴 임의 값이 없는가 (`var(--sys-*)` 참조는 허용)
|
|
665
736
|
- [ ] 텍스트에 `typo-*` 프리셋을 썼는가
|
|
666
737
|
- [ ] 간격이 전부 토큰 스케일 값인가 (`gap-13` ❌ → `gap-12` ✅)
|
|
667
|
-
- [ ] 본문이 12px(`typo-body-sm-default`)
|
|
738
|
+
- [ ] 본문이 12px(`typo-body-sm-default`)인가 (14px 본문 ❌)
|
|
739
|
+
- [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
|
|
668
740
|
- [ ] 페이지 패딩이 `p-20`, 블록·섹션 간격이 `gap-12` 인가 (`gap-16`/`gap-24` ❌)
|
|
669
741
|
- [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-24` 등)
|
|
670
742
|
- [ ] 페이지가 §4의 표준 골격에서 시작했는가
|
|
@@ -672,11 +744,13 @@ export default function ProductDetailPage() {
|
|
|
672
744
|
- [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
|
|
673
745
|
- [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
|
|
674
746
|
- [ ] 상태 표시에 `STag size="sm"` 을 썼는가
|
|
675
|
-
- [ ]
|
|
676
|
-
- [ ]
|
|
677
|
-
- [ ] 금액·수량 등 양을 나타내는 숫자에 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
|
|
747
|
+
- [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
|
|
748
|
+
- [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
|
|
749
|
+
- [ ] 금액·수량 등 양을 나타내는 숫자에 빠짐없이 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
|
|
678
750
|
- [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
|
|
679
|
-
- [ ]
|
|
751
|
+
- [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
|
|
752
|
+
- [ ] 버튼 `size` 가 위치 규칙과 맞는가 (행 내부 `xs` / 화면 액션 `sm` / 모달 푸터 `md`)
|
|
753
|
+
- [ ] 파괴 액션의 채움/outline 이 **적용 범위 + 위험도** 기준과 맞는가
|
|
680
754
|
- [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
|
|
681
755
|
- [ ] 상태 표시·알림·확인 다이얼로그가 §3의 선택 규칙을 따르는가
|
|
682
756
|
|
|
@@ -693,9 +767,8 @@ export default function ProductDetailPage() {
|
|
|
693
767
|
| `sellmate/no-arbitrary-class` | §1-2 토큰 있는 속성의 임의 값 (`text-[14px]`, `bg-[#eee]`) |
|
|
694
768
|
| `sellmate/no-off-scale-spacing` | §2-2 스케일 밖 간격 (`gap-13`) |
|
|
695
769
|
| `sellmate/table-numeric-align` | §3-4 숫자 컬럼의 `align: 'right'` 누락 (`--fix` 지원) |
|
|
696
|
-
| `sellmate/require-locale-number` | §1-4
|
|
770
|
+
| `sellmate/require-locale-number` | §1-4 금액·수량 등 수량 컬럼의 `toLocaleString()` 누락 |
|
|
697
771
|
| `sellmate/component-group-gap` | §2-2 컴포넌트 그룹 간격 (체크박스 가로 24 / 세로 8 등) |
|
|
698
|
-
| `sellmate/require-locale-number` | §1-4 금액·수량 등의 `toLocaleString()` 누락 |
|
|
699
772
|
| `sellmate/prefer-typo-preset` | §1-3 낱개 폰트 조합 (`text-14 font-bold`) |
|
|
700
773
|
|
|
701
774
|
`gap-13` 처럼 스케일 밖 값은 Tailwind v4 에서 **에러 없이 조용히 무시된다**. "간격이 왜 안 먹지" 로만 보이므로 반드시 스케일 값을 쓴다.
|
package/dist/llms.txt
CHANGED
|
@@ -127,7 +127,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
127
127
|
|
|
128
128
|
콤마 없는 `39000` 은 자릿수를 세어야 읽히지만 `39,000` 은 한눈에 읽힌다. 숫자를 그대로 출력하는 코드는 미완성으로 본다.
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
**번호·코드는 제외한다.** 전화번호·사업자번호·송장번호·상품코드처럼 대상을 가리키는 값은 크기를 비교하는 숫자가 아니라 **서식이 정해진 문자열**이다. 여기에 콤마를 넣으면 송장번호 `123456789` 가 `123,456,789` 로 보여 값 자체가 달라진다.
|
|
131
131
|
|
|
132
132
|
---
|
|
133
133
|
|
|
@@ -155,6 +155,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
155
155
|
|
|
156
156
|
페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
|
|
157
157
|
|
|
158
|
+
**보조 설명의 색** — 기본은 `text-fg-tertiary`(`grey_65`) 다. 보조 설명 안에서 위계가 한 단계 더 필요할 때만 `text-fg-secondary`(`grey_80`) → `text-fg-tertiary`(`grey_65`) 순으로 내려 쓴다 (§2-3).
|
|
159
|
+
|
|
158
160
|
### 2-2. 간격 (spacing)
|
|
159
161
|
|
|
160
162
|
- 유틸리티 숫자 = px. 스케일: `2 4 6 8 10 12 16 19 20 22 24 28 32 36 40 48 60`
|
|
@@ -166,8 +168,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
166
168
|
| **페이지 콘텐츠 패딩** | **`p-20`** |
|
|
167
169
|
| **섹션 ↔ 섹션** | **`gap-12`** |
|
|
168
170
|
| **페이지 내 블록 ↔ 블록** (헤더·필터·툴바·테이블 사이) | **`gap-12`** |
|
|
169
|
-
| 요소 ↔ 요소
|
|
170
|
-
| 제목 ↔ 설명 텍스트 | `gap-4
|
|
171
|
+
| 요소 ↔ 요소 | **기본 `gap-8`** (`--sys-space-stack-gap-element-normal`) · 타이트 `gap-4`(`-tight`) · 여유 `gap-12`(`-relaxed`) / `gap-16`(`-wide`) |
|
|
172
|
+
| 제목 ↔ 설명 텍스트 | **수직 배치 `gap-4`**(`--sys-space-stack-gap-text-normal`, 타이트 `gap-2`) · **가로 배치 `gap-8`**(`-relaxed`) |
|
|
171
173
|
|
|
172
174
|
정보 밀도가 높은 서비스라 블록 간격을 넓게 벌리지 않는다. `gap-16` / `gap-24` 를 페이지 골격의 기본값으로 쓰지 않는다.
|
|
173
175
|
|
|
@@ -207,23 +209,28 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
207
209
|
|
|
208
210
|
**시맨틱 유틸리티를 우선 사용한다** — 의미가 이름에 담긴 토큰이 이미 유틸리티로 존재한다.
|
|
209
211
|
|
|
210
|
-
#### 텍스트
|
|
212
|
+
#### 텍스트 회색 위계는 세 단계다
|
|
211
213
|
|
|
212
|
-
|
|
|
213
|
-
| --- | --- | --- |
|
|
214
|
-
|
|
|
215
|
-
| **보조
|
|
214
|
+
| 단계 | 유틸리티 | 값 | 용도 |
|
|
215
|
+
| --- | --- | --- | --- |
|
|
216
|
+
| **기본** | 지정하지 않는다 — 전역 기본색이 이미 적용된다 (`text-fg-primary`) | `grey_95` `#222222` | 본문·제목 |
|
|
217
|
+
| **보조 1** | **`text-fg-secondary`** | `grey_80` `#555555` | 보조 정보 |
|
|
218
|
+
| **보조 2 / 비활성** | **`text-fg-tertiary`** | `grey_65` `#888888` | 부가 설명, 비활성 |
|
|
216
219
|
|
|
220
|
+
- **위계는 순차 적용한다.** 기본 → 보조1 → 보조2 순으로 내려가며 중간 단계를 건너뛰지 않는다. 본문 바로 아래에 곧장 `text-fg-tertiary` 를 쓰지 않는다.
|
|
217
221
|
- `theme.css` 가 `body` 에 `grey_95` 를 깔아두므로 **본문에 텍스트 색 클래스를 붙이지 않는다.** `text-fg-primary` 를 매번 쓰는 것은 불필요하다.
|
|
218
|
-
-
|
|
219
|
-
-
|
|
222
|
+
- 위계가 한 단계면 충분한 보조 설명은 `text-fg-tertiary` 하나로 끝낸다 (§2-1). 두 단계가 필요할 때만 `text-fg-secondary` 를 끼워 넣는다.
|
|
223
|
+
- 비활성 표시는 단계와 무관하게 `text-fg-tertiary` 다.
|
|
220
224
|
|
|
221
225
|
```tsx
|
|
222
226
|
✅ <p>주문이 접수되었습니다.</p> // 색 지정 없음 = grey_95
|
|
223
|
-
✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명
|
|
227
|
+
✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명 한 단계
|
|
228
|
+
✅ <> // 보조 설명 두 단계
|
|
229
|
+
<p className="text-fg-secondary">배송비 정책</p>
|
|
230
|
+
<p className="text-fg-tertiary">3만원 이상 무료</p>
|
|
231
|
+
</>
|
|
224
232
|
✅ <span className="text-fg-tertiary">-</span> // 빈 값 (§3-4)
|
|
225
233
|
❌ <p className="text-fg-primary">주문이 접수되었습니다.</p> // 불필요
|
|
226
|
-
❌ <p className="text-fg-secondary">최근 30일 기준</p> // 중간 톤 사용
|
|
227
234
|
```
|
|
228
235
|
|
|
229
236
|
#### 그 밖의 색
|
|
@@ -292,7 +299,7 @@ function OrderModal({ orderId, open, onOpenChange, onClose, modalRef }: OrderMod
|
|
|
292
299
|
width={720}
|
|
293
300
|
// 주 액션은 button(단수), 보조 버튼은 footerLeft — 하단 버튼 양끝 분리 규칙과 같다
|
|
294
301
|
button={{ label: '접수', onClick: () => modalRef.ok() }}
|
|
295
|
-
footerLeft={<SButton color="
|
|
302
|
+
footerLeft={<SButton color="neutral" outline label="취소" onClick={() => modalRef.cancel()} />}
|
|
296
303
|
>
|
|
297
304
|
<SKeyValueTable fields={orderFields} values={order} />
|
|
298
305
|
</SActionModal>
|
|
@@ -307,19 +314,26 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
|
|
|
307
314
|
|
|
308
315
|
### 3-4. 테이블 컬럼 정렬
|
|
309
316
|
|
|
310
|
-
|
|
317
|
+
**값의 크기를 비교하는 숫자 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
|
|
311
318
|
자릿수가 세로로 맞아야 값의 크기를 눈으로 비교할 수 있기 때문이다.
|
|
312
319
|
|
|
313
|
-
|
|
320
|
+
**판별 기준은 "숫자인가"가 아니라 "크기를 비교하는가"다.** 자릿수 차이가 거의 없고 값끼리 대소를 견줄 일이 없으면 숫자로만 이루어져 있어도 우측 정렬하지 않는다.
|
|
321
|
+
|
|
322
|
+
| 값 성격 | 정렬 | 예 |
|
|
314
323
|
| --- | --- | --- |
|
|
315
|
-
| **금액·수량·개수·비율 등
|
|
316
|
-
|
|
|
324
|
+
| **금액·수량·개수·비율 등 양을 나타내는 값** | **`'right'`** | `39,000원` · `12개` · `3건` · `15%` |
|
|
325
|
+
| 코드·식별자 (주문번호, 상품코드, 순번) | **`'center'`** | `RV20250728-000010` · `1024` |
|
|
326
|
+
| 전화번호·사업자번호 | **`'center'`** | `010-1234-5678` |
|
|
327
|
+
| 일자·일시 | **`'center'`** | `2024-10-23` |
|
|
317
328
|
| 텍스트 | 생략(기본 `left`) | 상품명, 카테고리 |
|
|
318
329
|
| 상태 태그·아이콘·체크박스 등 고정폭 요소 | `'center'` | `STag`, `SIcon` |
|
|
319
330
|
|
|
331
|
+
**중앙 정렬은 `align: 'center'` 를 명시한다.** 기본값이 좌측이라 생략하면 중앙이 되지 않는다.
|
|
332
|
+
|
|
320
333
|
```tsx
|
|
321
334
|
const columns: STableColumn[] = [
|
|
322
|
-
{ name: '
|
|
335
|
+
{ name: 'orderNo', label: '주문번호', field: 'orderNo', width: '140px', align: 'center' },
|
|
336
|
+
{ name: 'orderedAt', label: '주문일자', field: 'orderedAt', width: '100px', align: 'center' },
|
|
323
337
|
{ name: 'name', label: '상품명', field: 'name' }, // 텍스트 → 생략
|
|
324
338
|
{ name: 'qty', label: '수량', field: 'qty', width: '80px', align: 'right',
|
|
325
339
|
format: (v: number) => `${Number(v).toLocaleString()}개` },
|
|
@@ -330,8 +344,16 @@ const columns: STableColumn[] = [
|
|
|
330
344
|
];
|
|
331
345
|
```
|
|
332
346
|
|
|
333
|
-
- `format` 으로 단위를 붙이더라도
|
|
334
|
-
- 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
|
|
347
|
+
- `format` 으로 단위를 붙이더라도 **양을 나타내면 오른쪽 정렬**이다. 단위 때문에 문자열이 되는 것은 정렬 판단과 무관하다.
|
|
348
|
+
- 양을 나타내는 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
|
|
349
|
+
- **번호·코드에는 세 자리 콤마를 넣지 않는다.** 송장번호 `123456789` 를 `123,456,789` 로 표시하면 값 자체가 달라 보인다.
|
|
350
|
+
- **헤더는 가운데, 셀만 우측**으로 두려면 `align` 이 아니라 `tdClass` 를 쓴다. `align` 은 `<th>` 와 `<td>` 에 함께 적용된다.
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
{ name: 'views', label: '조회수', field: 'views', align: 'center', tdClass: 'text-right!',
|
|
354
|
+
format: (v: number) => Number(v).toLocaleString() },
|
|
355
|
+
```
|
|
356
|
+
|
|
335
357
|
- `SKeyValueTable` 의 값 셀도 같은 기준을 따른다.
|
|
336
358
|
|
|
337
359
|
#### 값이 없는 셀은 회색 하이픈
|
|
@@ -359,13 +381,58 @@ const columns: STableColumn[] = [
|
|
|
359
381
|
|
|
360
382
|
| 상황 | 사용 |
|
|
361
383
|
| --- | --- |
|
|
362
|
-
| 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `size`: xs~
|
|
363
|
-
| **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** |
|
|
384
|
+
| 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `outline`, `size`: xs~md) |
|
|
385
|
+
| **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** (아래 3-5-3) |
|
|
364
386
|
| 텍스트만 있는 저강조 액션 | `SGhostButton` |
|
|
365
387
|
| 본문 속 이동 링크 | `STextLink` |
|
|
366
388
|
| 메인 액션 + 부가 메뉴 | `SDropdownButton` |
|
|
367
389
|
|
|
368
|
-
|
|
390
|
+
#### 3-5-1. 채움 버튼은 페이지당 개수 제한이 있다
|
|
391
|
+
|
|
392
|
+
`outline` 이 위계를 한 단계 낮춘다. **채움(outline 없음)이 그 페이지의 최상위 액션**이다.
|
|
393
|
+
|
|
394
|
+
| 조합 | 위계 | 페이지당 |
|
|
395
|
+
| --- | --- | --- |
|
|
396
|
+
| `color="primary"` (진남색 채움) | 최상위 실행 | **1개** |
|
|
397
|
+
| `color="danger"` (빨강 채움) | 최상위 파괴 | **1개** |
|
|
398
|
+
| `color="secondary"` (밝은 파랑 채움) | 중간 | N개 — **같은 속성 연속 배치 금지** |
|
|
399
|
+
| `primary outline` · `neutral outline` · `neutral`(흰 채움) · `danger outline` | 낮음 | N개 |
|
|
400
|
+
|
|
401
|
+
`SDropdownButton` 도 같은 규칙을 따르며, **페이지당 `primary` 채움 1개 계산에 포함**된다.
|
|
402
|
+
|
|
403
|
+
#### 3-5-2. `size` 는 놓이는 위치가 정한다
|
|
404
|
+
|
|
405
|
+
| 위치 | size |
|
|
406
|
+
| --- | --- |
|
|
407
|
+
| 테이블 **행 내부** 인라인 액션 | `xs` |
|
|
408
|
+
| 화면·목록 액션 (툴바, `STableBar`) | `sm` |
|
|
409
|
+
| **모달·위저드 푸터** | `md` |
|
|
410
|
+
|
|
411
|
+
`lg` 는 현재 사용처가 없다. 페이지 골격에서 쓰지 않는다.
|
|
412
|
+
|
|
413
|
+
#### 3-5-3. 파괴적 액션의 채움 / outline
|
|
414
|
+
|
|
415
|
+
물리적 위치가 아니라 **적용 범위 + 위험도**로 가른다.
|
|
416
|
+
|
|
417
|
+
| 상황 | 스타일 |
|
|
418
|
+
| --- | --- |
|
|
419
|
+
| 대상이 **전체**이거나 기능적 위험도가 높음 (계정 삭제, 전체 초기화) | `color="danger"` **채움** + 다른 버튼과 공간 분리 |
|
|
420
|
+
| **항목별**로 실행되거나 위험도가 상대적으로 낮음 (행 삭제, 연동 해제) | `color="danger" outline` |
|
|
421
|
+
|
|
422
|
+
#### 3-5-4. `SDropdownButton` — `split` 기준
|
|
423
|
+
|
|
424
|
+
| 설정 | 동작 | 쓰는 때 |
|
|
425
|
+
| --- | --- | --- |
|
|
426
|
+
| `split={false}` (기본) | 버튼 전체가 메뉴 토글. 클릭만으로는 실행되지 않는다 | 실행 전에 **범위·옵션을 골라야** 할 때 — `적치 지시서 출력 ▾` → 전체 항목 / 선택 항목만 |
|
|
427
|
+
| `split` | 좌측 = 기본 동작 **즉시 실행**, 우측 `⋯` = 변형 동작 | 기본 동작이 명확하고 **변형·부가 동작**을 곁들일 때 — `저장 \| 저장 후 신규 등록` |
|
|
428
|
+
|
|
429
|
+
메뉴 항목은 **기본 동작의 변형·부가 동작으로 한정**한다. 무관한 액션 여러 개를 한 드롭다운에 묶지 않는다.
|
|
430
|
+
|
|
431
|
+
#### 3-5-5. 그 밖
|
|
432
|
+
|
|
433
|
+
- 라벨은 **동사(+목적어)** — `저장` · `적치 지시` · `송장 재출력`
|
|
434
|
+
- 완료된 작업 버튼은 제거하지 않고 **`disabled` 로 남긴다.** 자리가 사라지면 행 높이가 흔들린다.
|
|
435
|
+
- **모달 보조 액션은 `color="neutral" outline`** 이 표준이다.
|
|
369
436
|
|
|
370
437
|
**하단 버튼 배치 (모든 화면 공통)** — 그룹을 모아 정렬하지 않고 **양끝으로 벌린다**(`justify-between`).
|
|
371
438
|
|
|
@@ -375,10 +442,10 @@ const columns: STableColumn[] = [
|
|
|
375
442
|
|
|
376
443
|
```tsx
|
|
377
444
|
<div className="flex items-center justify-between">
|
|
378
|
-
<SButton color="
|
|
445
|
+
<SButton color="neutral" outline label="취소" /> {/* 되돌리기 = 낮은 위계 */}
|
|
379
446
|
<div className="flex items-center gap-8">
|
|
380
447
|
<SCheckbox label="계속 등록하기" value={keep} onValueChange={setKeep} /> {/* 부가 요소 */}
|
|
381
|
-
<SButton label="저장" />
|
|
448
|
+
<SButton label="저장" /> {/* 페이지 유일한 primary 채움 */}
|
|
382
449
|
</div>
|
|
383
450
|
</div>
|
|
384
451
|
```
|
|
@@ -484,10 +551,14 @@ export default function ProductListPage() {
|
|
|
484
551
|
total={total}
|
|
485
552
|
selected={selected.length}
|
|
486
553
|
actions={
|
|
487
|
-
|
|
554
|
+
/* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
|
|
555
|
+
<SButton size="sm" color="danger" outline label="선택 삭제"
|
|
488
556
|
disabled={!selected.length} onClick={removeSelected} />
|
|
489
557
|
}
|
|
490
|
-
rightActions={
|
|
558
|
+
rightActions={
|
|
559
|
+
/* 이 페이지의 유일한 primary 채움 (§3-5-1) */
|
|
560
|
+
<SButton size="sm" label="상품 등록" onClick={goCreate} />
|
|
561
|
+
}
|
|
491
562
|
/>
|
|
492
563
|
|
|
493
564
|
<STable
|
|
@@ -557,7 +628,7 @@ export default function ProductCreatePage() {
|
|
|
557
628
|
|
|
558
629
|
{/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
|
|
559
630
|
<div className="flex items-center justify-between">
|
|
560
|
-
<SButton type="button" color="
|
|
631
|
+
<SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
|
|
561
632
|
<div className="flex items-center gap-8">
|
|
562
633
|
<SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
|
|
563
634
|
<SButton type="submit" label="저장" />
|
|
@@ -630,7 +701,7 @@ export default function ProductDetailPage() {
|
|
|
630
701
|
|
|
631
702
|
{/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
|
|
632
703
|
<div className="flex items-center justify-between">
|
|
633
|
-
<SButton color="
|
|
704
|
+
<SButton color="neutral" outline label="목록" onClick={goList} />
|
|
634
705
|
<div className="flex items-center gap-8">
|
|
635
706
|
<SButton color="danger" outline label="삭제" onClick={confirmDelete} />
|
|
636
707
|
<SButton label="수정" onClick={goEdit} />
|
|
@@ -665,7 +736,8 @@ export default function ProductDetailPage() {
|
|
|
665
736
|
- [ ] `text-[14px]`, `bg-[#...]` 같은 리터럴 임의 값이 없는가 (`var(--sys-*)` 참조는 허용)
|
|
666
737
|
- [ ] 텍스트에 `typo-*` 프리셋을 썼는가
|
|
667
738
|
- [ ] 간격이 전부 토큰 스케일 값인가 (`gap-13` ❌ → `gap-12` ✅)
|
|
668
|
-
- [ ] 본문이 12px(`typo-body-sm-default`)
|
|
739
|
+
- [ ] 본문이 12px(`typo-body-sm-default`)인가 (14px 본문 ❌)
|
|
740
|
+
- [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
|
|
669
741
|
- [ ] 페이지 패딩이 `p-20`, 블록·섹션 간격이 `gap-12` 인가 (`gap-16`/`gap-24` ❌)
|
|
670
742
|
- [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-24` 등)
|
|
671
743
|
- [ ] 페이지가 §4의 표준 골격에서 시작했는가
|
|
@@ -673,11 +745,13 @@ export default function ProductDetailPage() {
|
|
|
673
745
|
- [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
|
|
674
746
|
- [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
|
|
675
747
|
- [ ] 상태 표시에 `STag size="sm"` 을 썼는가
|
|
676
|
-
- [ ]
|
|
677
|
-
- [ ]
|
|
678
|
-
- [ ] 금액·수량 등 양을 나타내는 숫자에 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
|
|
748
|
+
- [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
|
|
749
|
+
- [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
|
|
750
|
+
- [ ] 금액·수량 등 양을 나타내는 숫자에 빠짐없이 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
|
|
679
751
|
- [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
|
|
680
|
-
- [ ]
|
|
752
|
+
- [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
|
|
753
|
+
- [ ] 버튼 `size` 가 위치 규칙과 맞는가 (행 내부 `xs` / 화면 액션 `sm` / 모달 푸터 `md`)
|
|
754
|
+
- [ ] 파괴 액션의 채움/outline 이 **적용 범위 + 위험도** 기준과 맞는가
|
|
681
755
|
- [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
|
|
682
756
|
- [ ] 상태 표시·알림·확인 다이얼로그가 §3의 선택 규칙을 따르는가
|
|
683
757
|
|
|
@@ -694,9 +768,8 @@ export default function ProductDetailPage() {
|
|
|
694
768
|
| `sellmate/no-arbitrary-class` | §1-2 토큰 있는 속성의 임의 값 (`text-[14px]`, `bg-[#eee]`) |
|
|
695
769
|
| `sellmate/no-off-scale-spacing` | §2-2 스케일 밖 간격 (`gap-13`) |
|
|
696
770
|
| `sellmate/table-numeric-align` | §3-4 숫자 컬럼의 `align: 'right'` 누락 (`--fix` 지원) |
|
|
697
|
-
| `sellmate/require-locale-number` | §1-4
|
|
771
|
+
| `sellmate/require-locale-number` | §1-4 금액·수량 등 수량 컬럼의 `toLocaleString()` 누락 |
|
|
698
772
|
| `sellmate/component-group-gap` | §2-2 컴포넌트 그룹 간격 (체크박스 가로 24 / 세로 8 등) |
|
|
699
|
-
| `sellmate/require-locale-number` | §1-4 금액·수량 등의 `toLocaleString()` 누락 |
|
|
700
773
|
| `sellmate/prefer-typo-preset` | §1-3 낱개 폰트 조합 (`text-14 font-bold`) |
|
|
701
774
|
|
|
702
775
|
`gap-13` 처럼 스케일 밖 값은 Tailwind v4 에서 **에러 없이 조용히 무시된다**. "간격이 왜 안 먹지" 로만 보이므로 반드시 스케일 값을 쓴다.
|
package/dist/styles.css
CHANGED
|
@@ -4,21 +4,57 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* 오른쪽
|
|
8
|
-
*
|
|
7
|
+
* 오른쪽 정렬·세 자리 콤마 대상 — 값의 "크기"를 비교하는 수량 컬럼.
|
|
8
|
+
*
|
|
9
|
+
* 식별자(주문번호·상품코드)는 자릿수가 있어도 대소를 비교하지 않으므로 뺀다.
|
|
10
|
+
* 우측 정렬의 근거가 "자릿수를 세로로 맞춰 크기를 읽는다"이고,
|
|
11
|
+
* 송장번호 `123456789` 에 콤마를 넣으면 `123,456,789` 라는 오표시가 된다.
|
|
9
12
|
*/
|
|
10
13
|
export const NUMERIC_LABEL =
|
|
11
|
-
|
|
14
|
+
/금액|가격|단가|판매가|원가|비용|수수료|배송비|할인|합계|총액|잔액|포인트|적립|수량|재고|개수|건수|횟수|비율|퍼센트|평점|점수|무게|용량|길이|면적|나이|연차/;
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* 식별자 — 숫자로 이루어져 있어도 크기 비교 대상이 아니다.
|
|
18
|
+
* 정렬·서식 규칙 모두에서 뺀다.
|
|
19
|
+
*
|
|
20
|
+
* (전화번호 010-1234-5678, 사업자번호 123-45-67890, 송장번호 123456789)
|
|
21
|
+
*
|
|
22
|
+
* `주문`·`오더` 같은 도메인 접두어는 넣지 않는다 — `주문 금액`·`오더 합계` 처럼
|
|
23
|
+
* 실제 수량 컬럼까지 함께 빠진다. 식별자 쪽은 뒤에 붙는 `번호`·`코드` 로 잡힌다.
|
|
16
24
|
*/
|
|
17
|
-
export const IDENTIFIER_ONLY =
|
|
25
|
+
export const IDENTIFIER_ONLY =
|
|
26
|
+
/전화|휴대폰|연락처|사업자|주민|계좌|우편|번호|코드|순번|순위|송장|운송장|전표|슬립|정산서|바코드|SKU|사번|일련/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 일자·일시 — 서식이 정해진 값이라 우측 정렬도 세 자리 콤마도 대상이 아니다 (§3-4 중앙 정렬).
|
|
30
|
+
* `format` 에 `Number(` 가 섞여도 숫자로 되살아나지 않게 따로 뺀다.
|
|
31
|
+
*/
|
|
32
|
+
export const DATE_LABEL = /일자|일시|날짜|기간|시각|등록일|수정일|생성일|만료일/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 상태·구분 컬럼 — 수량 어휘를 부분 문자열로 포함해도 값은 숫자가 아니다.
|
|
36
|
+
* (`비용 입력 여부` 가 `비용` 에 걸리는 것을 막는다)
|
|
37
|
+
*/
|
|
38
|
+
export const NON_NUMERIC_SUFFIX = /(여부|유무|상태|구분|타입|종류|명|이름)$/;
|
|
18
39
|
|
|
19
40
|
/** format 본문에 숫자 서식이 있으면 숫자 컬럼으로 본다 */
|
|
20
41
|
export const NUMERIC_FORMAT = /toLocaleString|toFixed|Number\s*\(|parseInt|parseFloat/;
|
|
21
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 셀을 우측 정렬하는 클래스.
|
|
45
|
+
* `align` 은 헤더·셀에 공통 적용되므로 "헤더 가운데 + 셀 우측" 은 `tdClass` 로만 표현된다.
|
|
46
|
+
*/
|
|
47
|
+
export const CELL_RIGHT_CLASS = /\btext-right\b/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 수량 판정에서 아예 빼야 하는 라벨인지 — 식별자·일자·상태/구분 컬럼.
|
|
51
|
+
* 라벨 분기·format 분기보다 먼저 확인해야 한다.
|
|
52
|
+
* (`사업자번호` 처럼 식별자여도 format 에 `Number(` 가 있으면 숫자로 되돌아간다)
|
|
53
|
+
*/
|
|
54
|
+
export function isNonQuantityLabel(label) {
|
|
55
|
+
return IDENTIFIER_ONLY.test(label) || DATE_LABEL.test(label) || NON_NUMERIC_SUFFIX.test(label);
|
|
56
|
+
}
|
|
57
|
+
|
|
22
58
|
/** 객체 리터럴에서 프로퍼티 노드를 이름으로 찾는다 */
|
|
23
59
|
export function prop(objectNode, name) {
|
|
24
60
|
return objectNode.properties.find(
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 숫자 컬럼은 예외 없이 `toLocaleString()` 으로 세 자리 콤마를 넣도록 강제한다.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 문자열이므로 대상에서
|
|
4
|
+
* 전화번호·송장번호처럼 식별자로 쓰이는 값은 크기를 비교하는 숫자가 아니라
|
|
5
|
+
* 서식이 정해진 문자열이므로 대상에서 뺀다 (§5 체크리스트 "번호·코드는 제외").
|
|
6
|
+
* 그 밖에 예외가 필요하면 `allow` 로 컬럼을 지정한다.
|
|
6
7
|
*
|
|
7
8
|
* AGENTS.md §1-4 / §3-4.
|
|
8
9
|
*/
|
|
9
10
|
import {
|
|
10
11
|
NUMERIC_LABEL,
|
|
11
|
-
|
|
12
|
+
isNonQuantityLabel,
|
|
12
13
|
prop,
|
|
13
14
|
asColumn,
|
|
14
15
|
} from "../lib/table-column.mjs";
|
|
@@ -48,11 +49,12 @@ export default {
|
|
|
48
49
|
const { labelProp, label, columnName } = column;
|
|
49
50
|
if (columnName && allow.has(columnName)) return;
|
|
50
51
|
|
|
51
|
-
//
|
|
52
|
-
if (!NUMERIC_LABEL.test(label) ||
|
|
52
|
+
// 수량 컬럼이면 모두 대상 — 식별자(전화번호·송장번호)와 상태 컬럼은 뺀다
|
|
53
|
+
if (!NUMERIC_LABEL.test(label) || isNonQuantityLabel(label)) return;
|
|
53
54
|
|
|
54
|
-
// render 로 셀을 직접 그리는 경우는 이 규칙이 판단하지 않는다
|
|
55
|
-
|
|
55
|
+
// render / renderCell 로 셀을 직접 그리는 경우는 이 규칙이 판단하지 않는다
|
|
56
|
+
// (renderCell 은 3.x STable 의 `<td>` 커스텀 렌더 — 2.2.0 에서 추가됐다)
|
|
57
|
+
if (prop(node, "render") || prop(node, "renderCell")) return;
|
|
56
58
|
|
|
57
59
|
const unit = /금액|가격|단가|판매가|원가|비용|수수료|배송비|할인|합계|총액|잔액/.test(label)
|
|
58
60
|
? "원"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 자릿수가 세로로 맞아야 값의 크기를 비교할 수 있으므로,
|
|
5
5
|
* 금액·수량·개수처럼 숫자가 들어가는 컬럼은 예외 없이 오른쪽 정렬한다.
|
|
6
|
+
* 크기를 비교하지 않는 식별자(주문번호·상품코드)는 대상이 아니다.
|
|
6
7
|
*
|
|
7
8
|
* 대상은 `name` · `label` · `field` 를 모두 가진 객체 리터럴(= STableColumn 형태)뿐이라
|
|
8
9
|
* 일반 객체를 오탐하지 않는다.
|
|
@@ -12,8 +13,9 @@
|
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
NUMERIC_LABEL,
|
|
15
|
-
IDENTIFIER_ONLY,
|
|
16
16
|
NUMERIC_FORMAT,
|
|
17
|
+
CELL_RIGHT_CLASS,
|
|
18
|
+
isNonQuantityLabel,
|
|
17
19
|
prop,
|
|
18
20
|
literalString,
|
|
19
21
|
asColumn,
|
|
@@ -53,13 +55,18 @@ export default {
|
|
|
53
55
|
const { labelProp, label, columnName } = column;
|
|
54
56
|
if (columnName && allow.has(columnName)) return;
|
|
55
57
|
|
|
58
|
+
// 식별자(송장번호)·상태 컬럼(비용 입력 여부)은 크기를 비교하지 않는다.
|
|
59
|
+
// label·format 두 분기보다 먼저 빼야 format 의 `Number(` 로 되살아나지 않는다.
|
|
60
|
+
if (isNonQuantityLabel(label)) return;
|
|
61
|
+
|
|
62
|
+
// render/renderCell 로 셀을 직접 그리면 라벨만으로 숫자 여부를 알 수 없다
|
|
63
|
+
// (`정산서 번호` → 링크, `순번` → 핀 아이콘). 실제 숫자 서식이 있을 때만 숫자로 본다.
|
|
64
|
+
const renderProp = prop(node, "renderCell") ?? prop(node, "render");
|
|
65
|
+
if (renderProp && !NUMERIC_FORMAT.test(context.sourceCode.getText(renderProp))) return;
|
|
66
|
+
|
|
56
67
|
// 숫자 판정 — label 키워드 또는 format 의 숫자 서식
|
|
57
68
|
let reason = "";
|
|
58
|
-
let numeric =
|
|
59
|
-
|
|
60
|
-
if (NUMERIC_LABEL.test(label) && !IDENTIFIER_ONLY.test(label)) {
|
|
61
|
-
numeric = true;
|
|
62
|
-
}
|
|
69
|
+
let numeric = NUMERIC_LABEL.test(label);
|
|
63
70
|
|
|
64
71
|
const formatProp = prop(node, "format");
|
|
65
72
|
if (formatProp) {
|
|
@@ -75,6 +82,10 @@ export default {
|
|
|
75
82
|
const alignProp = prop(node, "align");
|
|
76
83
|
if (literalString(alignProp) === "right") return;
|
|
77
84
|
|
|
85
|
+
// align 은 헤더·셀 공통이라 "헤더 가운데 + 셀 우측" 은 tdClass 로만 표현된다
|
|
86
|
+
const tdClassProp = prop(node, "tdClass");
|
|
87
|
+
if (tdClassProp && CELL_RIGHT_CLASS.test(context.sourceCode.getText(tdClassProp))) return;
|
|
88
|
+
|
|
78
89
|
context.report({
|
|
79
90
|
node: alignProp ?? labelProp,
|
|
80
91
|
messageId: "needsRight",
|