@m1kapp/kit 0.0.23 → 0.0.25
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 +221 -2
- package/bin/claim.mjs +77 -0
- package/bin/m1kkit.mjs +16 -0
- package/bin/skills/m1kapp-init.md +108 -92
- package/bin/stats.mjs +211 -0
- package/bin/track.mjs +95 -0
- package/dist/index.d.mts +652 -4
- package/dist/index.d.ts +652 -4
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/dist/meta.json +325 -0
- package/dist/pwa.js +2 -2
- package/dist/pwa.mjs +2 -2
- package/dist/styles.css +1 -1
- package/dist/utils.d.mts +17 -1
- package/dist/utils.d.ts +17 -1
- package/dist/utils.js +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,9 +40,11 @@ export default function App() {
|
|
|
40
40
|
|
|
41
41
|
## 모듈 구성
|
|
42
42
|
|
|
43
|
+
> **하나의 import면 끝.** UI · OG · PWA · Fetch · Utils가 전부 `@m1kapp/kit` 메인 export에 들어 있습니다. `/server`만 별도 서브패스예요. 서브패스를 일일이 외울 필요 없이 `import { ... } from "@m1kapp/kit"` 하나로 다 꺼내 쓰세요.
|
|
44
|
+
|
|
43
45
|
| 모듈 | import | 설명 |
|
|
44
46
|
|---|---|---|
|
|
45
|
-
| UI | `@m1kapp/kit` | 컴포넌트
|
|
47
|
+
| UI | `@m1kapp/kit` | 컴포넌트 45개 + 훅 |
|
|
46
48
|
| OG Image | `@m1kapp/kit` | OG 이미지 생성 (서버) |
|
|
47
49
|
| PWA | `@m1kapp/kit` | manifest, viewport, 설치 유도 |
|
|
48
50
|
| Fetch | `@m1kapp/kit` | 캐싱·중복제거·재시도 fetch 유틸 |
|
|
@@ -51,6 +53,27 @@ export default function App() {
|
|
|
51
53
|
|
|
52
54
|
---
|
|
53
55
|
|
|
56
|
+
## 레시피 — 이럴 땐 이거
|
|
57
|
+
|
|
58
|
+
손으로 만들기 전에 먼저 찾아보세요. 흔한 화면은 대부분 조합으로 끝납니다.
|
|
59
|
+
|
|
60
|
+
| 하고 싶은 것 | 이렇게 |
|
|
61
|
+
|---|---|
|
|
62
|
+
| 모바일 앱 셸 | `AppShell` + `AppShellHeader` / `AppShellContent` + `TabBar`/`Tab` + `Watermark` |
|
|
63
|
+
| 목록 + 로딩 + 빈 화면 | `useFetch` → 로딩이면 `Skeleton`, 빈 배열이면 `EmptyState` |
|
|
64
|
+
| 폼 저장 후 피드백 | `useFormSubmit` + `useToast` (인라인 텍스트 대신 토스트) |
|
|
65
|
+
| 설정 화면 | `Section` + `Field`(`inline`) + `Switch` |
|
|
66
|
+
| 인라인 토글 (오늘/이번주) | `SegmentedControl` (하단 글로벌 내비는 `TabBar`) |
|
|
67
|
+
| 채팅 / AI 비서 | `MessageList` + `ChatBubble` + `TypingIndicator` |
|
|
68
|
+
| "이렇게 할까요?" 확인 | `ActionCard` (메시지 흐름 안), 모달이면 `Dialog` |
|
|
69
|
+
| 일정 / 타임라인 행 | `ListRow` (`sizeByMinutes`로 소요시간 비례 높이) |
|
|
70
|
+
| 메모 속 URL 링크 | `LinkifiedText` |
|
|
71
|
+
| API 호출 | `createApiClient` (`get`/`post`/`put`/`delete` + `ApiError`) |
|
|
72
|
+
| 주기적 갱신 | `usePolling` (`pauseOnHidden`) |
|
|
73
|
+
| 테마색 한 번에 | `<AppShell accent="#e2603f">` → 모든 컴포넌트가 `--kit-accent`로 따라옴 |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
54
77
|
## UI
|
|
55
78
|
|
|
56
79
|
CSS가 import 시 자동 주입됩니다. 별도 스타일시트 import 불필요.
|
|
@@ -246,6 +269,169 @@ colors.green // "#22c55e"
|
|
|
246
269
|
// blue | purple | green | orange | pink | red | yellow | cyan | slate | zinc
|
|
247
270
|
```
|
|
248
271
|
|
|
272
|
+
**임의 색도 OK.** 팔레트는 프리셋일 뿐이고, `ThemeDialog`의 `palette` / `onSelect`는 어떤 hex든 받습니다. 그리고 `Switch`·`SegmentedControl`·`ChatBubble`·`ActionCard`·`ListRow`·`LinkifiedText`의 accent는 `--kit-accent` CSS 변수를 따라가므로, 한 곳만 바꾸면 전체 톤이 바뀝니다.
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
// ① AppShell accent prop — 가장 쉬움 (하위 전체에 적용)
|
|
276
|
+
<AppShell accent="#e2603f">...</AppShell>
|
|
277
|
+
|
|
278
|
+
// ② 아무 래퍼에나 CSS 변수로
|
|
279
|
+
<div style={{ "--kit-accent": "#e2603f" } as React.CSSProperties}>...</div>
|
|
280
|
+
|
|
281
|
+
// ③ 컴포넌트별 override
|
|
282
|
+
<Switch checked={on} onChange={setOn} accent="#e2603f" />
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 폼 · 설정
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
import { Field, Switch } from "@m1kapp/kit";
|
|
289
|
+
|
|
290
|
+
// 라벨드 인풋 (stacked 기본 / inline / multiline)
|
|
291
|
+
<Field label="이름" value={name} onChange={setName} placeholder="이름" />
|
|
292
|
+
<Field label="이메일" value={email} inline readOnly />
|
|
293
|
+
<Field label="메모" value={memo} onChange={setMemo} multiline rows={3} hint="자유롭게 적어주세요" />
|
|
294
|
+
|
|
295
|
+
// on/off 토글 — accent는 --kit-accent 자동 연동
|
|
296
|
+
<Switch checked={on} onChange={setOn} aria-label="알림" />
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 세그먼트 토글
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import { SegmentedControl } from "@m1kapp/kit";
|
|
303
|
+
|
|
304
|
+
<SegmentedControl
|
|
305
|
+
value={view}
|
|
306
|
+
onChange={setView}
|
|
307
|
+
options={[{ value: "today", label: "오늘" }, { value: "week", label: "이번 주" }]}
|
|
308
|
+
/>
|
|
309
|
+
// 인라인 토글용. 하단 글로벌 내비게이션은 TabBar를 쓰세요.
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 채팅 / 대화
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
import { MessageList, ChatBubble, TypingIndicator } from "@m1kapp/kit";
|
|
316
|
+
import type { ChatMessage } from "@m1kapp/kit";
|
|
317
|
+
|
|
318
|
+
const messages: ChatMessage[] = [
|
|
319
|
+
{ role: "user", content: "내일 3시 회의 잡아줘", timestamp: Date.now() },
|
|
320
|
+
{ role: "assistant", content: "네, 잡아드릴게요." },
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
// dayDivider: timestamp 기준 날짜가 바뀌면 구분선 자동 삽입
|
|
324
|
+
<MessageList messages={messages} dayDivider>
|
|
325
|
+
{pending && <TypingIndicator />}
|
|
326
|
+
</MessageList>
|
|
327
|
+
|
|
328
|
+
// 직접 쓰려면
|
|
329
|
+
<ChatBubble role="user">오늘 일정 보여줘</ChatBubble>
|
|
330
|
+
<ChatBubble role="assistant">3건 있어요.</ChatBubble>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 확인 카드 (propose → confirm → execute)
|
|
334
|
+
|
|
335
|
+
LLM 툴콜처럼 "제안 → 확인 → 실행" 흐름을 메시지 안에 박을 때. 모달이 아니라 흐름에 인라인됩니다.
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import { ActionCard } from "@m1kapp/kit";
|
|
339
|
+
|
|
340
|
+
const [state, setState] = useState<"pending" | "loading" | "done" | "cancelled">("pending");
|
|
341
|
+
|
|
342
|
+
<ActionCard
|
|
343
|
+
title="이렇게 기록해둘까요?"
|
|
344
|
+
state={state}
|
|
345
|
+
items={["🗓 6/4 15:00 디자인 리뷰", "📌 6/4 (종일) 마감일"]}
|
|
346
|
+
onConfirm={() => { setState("loading"); /* … */ setState("done"); }}
|
|
347
|
+
onCancel={() => setState("cancelled")}
|
|
348
|
+
/>
|
|
349
|
+
// state별 색/문구 자동: pending → done(초록) / cancelled(취소선) / loading
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 리스트 / 타임라인 행
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
import { ListRow } from "@m1kapp/kit";
|
|
356
|
+
|
|
357
|
+
<ListRow
|
|
358
|
+
accent="#7fc06a" // 왼쪽 컬러바 (생략 시 --kit-accent)
|
|
359
|
+
lead="14:00" leadSub="15:00"
|
|
360
|
+
title="디자인 리뷰" sub="👥 김상훈"
|
|
361
|
+
trailing="● 지금" active // 현재 항목 강조
|
|
362
|
+
heightScale={60 / 30} // 높이 배율 (1=기본, 46–130px). 일정은 분/30을 넘김
|
|
363
|
+
onClick={open}
|
|
364
|
+
/>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 텍스트 내 링크
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
import { LinkifiedText } from "@m1kapp/kit";
|
|
371
|
+
|
|
372
|
+
<LinkifiedText>{"회의록: https://docs.google.com/…\n화상: meet.google.com/abc-def"}</LinkifiedText>
|
|
373
|
+
// http(s) URL + (기본) meet.google.com 같은 도메인/경로를 자동 링크. 줄바꿈 보존.
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 진행 표시 · 단계
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
import { Stepper, Collapsible, ProgressRing } from "@m1kapp/kit";
|
|
380
|
+
|
|
381
|
+
// 다단계 진행 표시 (위저드/멀티페이즈 플로우)
|
|
382
|
+
<Stepper current={phase} onStepClick={setPhase} steps={[
|
|
383
|
+
{ label: "분석", icon: "📝" }, { label: "준비", icon: "📸" }, { label: "생성", icon: "✨" },
|
|
384
|
+
]} />
|
|
385
|
+
|
|
386
|
+
// 접기 카드 (헤더+배지+상태)
|
|
387
|
+
<Collapsible leading={1} title="페르소나 분석" subtitle="스타일 파악" completed
|
|
388
|
+
open={open === 1} onToggle={() => setOpen(open === 1 ? null : 1)}>
|
|
389
|
+
<p>본문…</p>
|
|
390
|
+
</Collapsible>
|
|
391
|
+
|
|
392
|
+
// 원형 진행률
|
|
393
|
+
<ProgressRing value={7} max={10}><span className="text-2xl font-black">7</span></ProgressRing>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 입력 · 편집
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import { Select, ColorPicker, InlineEdit } from "@m1kapp/kit";
|
|
400
|
+
|
|
401
|
+
// 앵커드 드롭다운 (카운트·비활성 옵션, 외부클릭/ESC 닫힘)
|
|
402
|
+
<Select value={cat} onChange={setCat} placeholder="난이도" options={[
|
|
403
|
+
{ value: "a", label: "초급", count: 12 }, { value: "b", label: "고급", count: 0, disabled: true },
|
|
404
|
+
]} />
|
|
405
|
+
|
|
406
|
+
// 컬러 피커 (프리셋 + 커스텀 hex)
|
|
407
|
+
<ColorPicker value={color} onChange={setColor} />
|
|
408
|
+
|
|
409
|
+
// 탭하여 편집 (Enter 저장 · Esc 취소)
|
|
410
|
+
<InlineEdit value={name} onChange={rename} className="text-lg font-bold" />
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### 복사 · 데이터
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
import { CopyButton, CodeBlock, useCopy, BarList, Countdown, Carousel, Img } from "@m1kapp/kit";
|
|
417
|
+
|
|
418
|
+
<CopyButton text="npm i @m1kapp/kit">설치 복사</CopyButton>
|
|
419
|
+
<CodeBlock label="install" code="npm i @m1kapp/kit" />
|
|
420
|
+
const { copied, copy } = useCopy(); // keyed: copy(text, "row-3")
|
|
421
|
+
|
|
422
|
+
// 가로 막대 분석 차트
|
|
423
|
+
<BarList items={[{ label: "/", value: 120 }, { label: "/about", value: 64, href: "/about" }]} />
|
|
424
|
+
|
|
425
|
+
// 카운트다운 (D-day)
|
|
426
|
+
<Countdown to="2026-12-31" onComplete={celebrate} />
|
|
427
|
+
|
|
428
|
+
// 스와이프 캐러셀 (controlled, 점 인디케이터)
|
|
429
|
+
<Carousel count={slides.length} index={i} onChange={setI}>{slides[i]}</Carousel>
|
|
430
|
+
|
|
431
|
+
// 다중 URL 폴백 이미지
|
|
432
|
+
<Img candidates={[avatarUrl, gravatar]} fallback={<Avatar fallback="MH" />} className="h-10 w-10 rounded-full" />
|
|
433
|
+
```
|
|
434
|
+
|
|
249
435
|
### 워터마크
|
|
250
436
|
|
|
251
437
|
```tsx
|
|
@@ -256,6 +442,32 @@ import { Watermark } from "@m1kapp/kit";
|
|
|
256
442
|
</Watermark>
|
|
257
443
|
```
|
|
258
444
|
|
|
445
|
+
`Watermark`는 하단에 `PoweredByKit` 크레딧을 자동 내장합니다.
|
|
446
|
+
|
|
447
|
+
### 방문자 트래커 (m1k.app) — 선택, 기본 OFF
|
|
448
|
+
|
|
449
|
+
`Watermark`/`PoweredByKit` 하단 크레딧이 방문자 수를 집계할 수 있습니다. **slug가 있을 때만** 켜지고, 없으면 아무것도 전송하지 않아요(기본 off). 집계는 페이지뷰 카운트뿐 — PII 없음.
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
# 1) 사이트 등록 (무로그인) → slug 발급
|
|
453
|
+
npx m1kkit track https://myside.app
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
# 2) .env 에 한 줄 → 이후 자동 집계 (스니펫 붙여넣기 불필요)
|
|
458
|
+
NEXT_PUBLIC_M1K_SLUG=your-slug
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
// 끄기 / 명시 지정
|
|
463
|
+
<Watermark track={false}>…</Watermark> // 비콘 끔
|
|
464
|
+
<Watermark trackSlug="your-slug">…</Watermark> // env 대신 직접 지정
|
|
465
|
+
// PoweredByKit 단독 사용 시: <PoweredByKit slug="your-slug" track={false} />
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
> `npx m1kkit claim` 으로 익명 등록 사이트를 내 m1k.app 계정에 귀속할 수 있어요.
|
|
469
|
+
> (`.m1k.json`의 일회용 토큰 + 로그인으로 인증.) 귀속하는 김에 **kit이 이 프로젝트에서 코드를 얼마나 아껴줬는지** 분석도 같이 출력해요 — `--no-stats`로 끌 수 있어요.
|
|
470
|
+
|
|
259
471
|
---
|
|
260
472
|
|
|
261
473
|
## OG Image
|
|
@@ -545,11 +757,18 @@ export const GET = handler(async () => {
|
|
|
545
757
|
순수 함수 — 의존성 없음, 어디서나 import.
|
|
546
758
|
|
|
547
759
|
```ts
|
|
548
|
-
import { relativeTime, formatNumber, formatPrice, cn } from "@m1kapp/kit";
|
|
760
|
+
import { relativeTime, formatNumber, formatPrice, cn, formatDuration, groupByDay } from "@m1kapp/kit";
|
|
549
761
|
|
|
550
762
|
// 상대 시간
|
|
551
763
|
relativeTime(post.createdAt) // "3분 전", "어제", "2025. 4. 19."
|
|
552
764
|
|
|
765
|
+
// 소요 시간 포맷
|
|
766
|
+
formatDuration(90_000) // "1분 30초"
|
|
767
|
+
formatDuration(3_661_000, { style: "clock" }) // "1:01:01"
|
|
768
|
+
|
|
769
|
+
// 날짜별 그룹핑 — [{ date, label: "오늘"|"어제"|"4월 19일 (토)", items }]
|
|
770
|
+
groupByDay(logs, (l) => l.timestamp)
|
|
771
|
+
|
|
553
772
|
// 숫자 포맷
|
|
554
773
|
formatNumber(1_500) // "1.5천"
|
|
555
774
|
formatNumber(15_000) // "1.5만"
|
package/bin/claim.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* m1kkit claim [--token=xxx] [--host=m1k.app] [--no-stats]
|
|
4
|
+
*
|
|
5
|
+
* 익명 등록한 사이트를 내 계정에 귀속한다.
|
|
6
|
+
* claim은 로그인(브라우저 세션)이 필요하므로, 토큰을 들고 브라우저의 claim 페이지를 연다.
|
|
7
|
+
* 토큰은 --token 또는 ./.m1k.json 에서 읽는다.
|
|
8
|
+
* 귀속하는 김에 'stats' 분석도 같이 돌려 kit이 얼마나 아껴줬는지 보여준다 (--no-stats 로 끔).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { resolve, dirname, join } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { exec, spawnSync } from "child_process";
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const flags = Object.fromEntries(
|
|
17
|
+
args.filter((a) => a.startsWith("--")).map((a) => {
|
|
18
|
+
const [k, v] = a.replace(/^--/, "").split("=");
|
|
19
|
+
return [k, v ?? true];
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let token = flags.token;
|
|
24
|
+
let host = flags.host || process.env.M1K_HOST || "m1k.app";
|
|
25
|
+
|
|
26
|
+
if (!token) {
|
|
27
|
+
const file = resolve(process.cwd(), ".m1k.json");
|
|
28
|
+
if (existsSync(file)) {
|
|
29
|
+
try {
|
|
30
|
+
const store = JSON.parse(readFileSync(file, "utf8"));
|
|
31
|
+
token = store.claimToken;
|
|
32
|
+
host = flags.host || store.host || host;
|
|
33
|
+
} catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!token || flags.help) {
|
|
38
|
+
console.log(`
|
|
39
|
+
m1kkit claim — 익명 등록 사이트를 내 계정에 귀속
|
|
40
|
+
|
|
41
|
+
사용법:
|
|
42
|
+
npx m1kkit claim [--token=<claimToken>] [--host=m1k.app] [--no-stats]
|
|
43
|
+
|
|
44
|
+
토큰을 안 주면 현재 폴더의 ./.m1k.json 에서 읽어요.
|
|
45
|
+
귀속은 로그인이 필요해서 브라우저의 claim 페이지를 엽니다.
|
|
46
|
+
귀속하는 김에 코드 분석(stats)도 같이 돌려 kit이 얼마나 아껴줬는지 보여줘요. (--no-stats 로 끔)
|
|
47
|
+
`);
|
|
48
|
+
process.exit(token ? 0 : 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const scheme = /^(localhost|127\.|0\.0\.0\.0)/.test(host) ? "http" : "https";
|
|
52
|
+
const claimUrl = `${scheme}://${host}/claim?token=${encodeURIComponent(token)}`;
|
|
53
|
+
console.log(`\n브라우저에서 로그인 후 귀속하세요:\n ${claimUrl}\n`);
|
|
54
|
+
|
|
55
|
+
// 플랫폼별 브라우저 열기 (실패해도 URL은 위에 출력됨)
|
|
56
|
+
const opener =
|
|
57
|
+
process.platform === "darwin" ? "open" :
|
|
58
|
+
process.platform === "win32" ? 'start ""' :
|
|
59
|
+
"xdg-open";
|
|
60
|
+
exec(`${opener} "${claimUrl}"`, (err) => {
|
|
61
|
+
if (err) console.log("(브라우저 자동 실행 실패 — 위 URL을 직접 열어주세요)");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 인증하는 김에 — 이 프로젝트에서 kit이 얼마나 아껴줬는지 같이 분석 (--no-stats 로 끔)
|
|
65
|
+
if (!flags["no-stats"]) {
|
|
66
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
console.log("\n📊 그리고 — 이 프로젝트에서 kit이 얼마나 아껴줬는지 볼게요 👇\n");
|
|
68
|
+
const statsArgs = [join(here, "stats.mjs")];
|
|
69
|
+
if (typeof flags.dir === "string") statsArgs.push(`--dir=${flags.dir}`);
|
|
70
|
+
if (typeof flags.out === "string") statsArgs.push(`--out=${flags.out}`);
|
|
71
|
+
const r = spawnSync(process.execPath, statsArgs, { stdio: "inherit" });
|
|
72
|
+
if (r.status !== 0) {
|
|
73
|
+
console.log("\n(분석은 건너뛰었어요 — 나중에 'npx m1kkit stats' 로 직접 실행할 수 있어요)");
|
|
74
|
+
} else {
|
|
75
|
+
console.log("\n✓ 분석 결과는 kit-stats.json 에 저장됐고, 하단 'powered by' 크레딧 시트에서도 보여요.");
|
|
76
|
+
}
|
|
77
|
+
}
|
package/bin/m1kkit.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Commands:
|
|
6
6
|
* m1kkit favicon — 파비콘 생성
|
|
7
7
|
* m1kkit skills — Claude Code 스킬 설치
|
|
8
|
+
* m1kkit stats — 코드 분석 & kit 사용 현황 생성
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const [,, command, ...rest] = process.argv;
|
|
@@ -16,6 +17,9 @@ m1kkit — @m1kapp/kit CLI
|
|
|
16
17
|
Commands:
|
|
17
18
|
m1kkit favicon [options] 파비콘 자동 생성
|
|
18
19
|
m1kkit skills [options] Claude Code 스킬 설치
|
|
20
|
+
m1kkit stats [options] 코드 분석 & kit 사용 현황 생성
|
|
21
|
+
m1kkit track <url> m1k 방문자 트래커에 사이트 등록(무로그인)
|
|
22
|
+
m1kkit claim [--token=x] 등록한 사이트를 내 계정에 귀속
|
|
19
23
|
|
|
20
24
|
Options:
|
|
21
25
|
--help 도움말 보기
|
|
@@ -25,6 +29,9 @@ Examples:
|
|
|
25
29
|
m1kkit skills
|
|
26
30
|
m1kkit skills --list
|
|
27
31
|
m1kkit skills m1kapp-init
|
|
32
|
+
m1kkit stats --dir=src --out=public
|
|
33
|
+
m1kkit track https://myside.app
|
|
34
|
+
m1kkit claim
|
|
28
35
|
`);
|
|
29
36
|
process.exit(0);
|
|
30
37
|
}
|
|
@@ -42,6 +49,15 @@ if (command === "favicon") {
|
|
|
42
49
|
} else if (command === "skills") {
|
|
43
50
|
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
44
51
|
await import(path.join(__dirname, "skills.mjs"));
|
|
52
|
+
} else if (command === "stats") {
|
|
53
|
+
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
54
|
+
await import(path.join(__dirname, "stats.mjs"));
|
|
55
|
+
} else if (command === "track") {
|
|
56
|
+
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
57
|
+
await import(path.join(__dirname, "track.mjs"));
|
|
58
|
+
} else if (command === "claim") {
|
|
59
|
+
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
60
|
+
await import(path.join(__dirname, "claim.mjs"));
|
|
45
61
|
} else {
|
|
46
62
|
console.error(`알 수 없는 커맨드: ${command}`);
|
|
47
63
|
console.error("사용법: m1kkit --help");
|
|
@@ -1,18 +1,60 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: m1kapp-init
|
|
3
|
-
description: "@m1kapp/kit 기반 Next.js 프로젝트 초기 설정을 인터랙티브하게 완성합니다."
|
|
3
|
+
description: "@m1kapp/kit 기반 Next.js 프로젝트 초기 설정을 인터랙티브하게 완성합니다. (앱쉘 레이아웃 + SEO + PWA + OG + 파비콘)"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
현재 디렉토리가 Next.js + @m1kapp/kit 프로젝트인지 확인한 뒤, 아래 순서대로 진행한다.
|
|
7
7
|
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⚠️ 핵심 규칙 — 매번 여기서 틀린다. 먼저 읽어라.
|
|
11
|
+
|
|
12
|
+
@m1kapp/kit은 단순 SEO 유틸이 아니라 **모바일 앱 셸 UI 프레임워크**다. 아래는 한 번에 되게 하려면 반드시 지킬 것:
|
|
13
|
+
|
|
14
|
+
1. **`app/layout.tsx`에 스타일/셸 import 3종 세트는 필수다.** 하나라도 빠지면 컴포넌트가 스타일 없이 깨지거나 좌측 정렬된다.
|
|
15
|
+
```ts
|
|
16
|
+
import "@m1kapp/kit/styles.css"; // ← 없으면 AppShell/모든 컴포넌트 스타일 안 먹음 (제일 자주 빠뜨림)
|
|
17
|
+
import { KitStyles, mobileViewport } from "@m1kapp/kit/pwa";
|
|
18
|
+
// <head> 안에 <KitStyles />, 그리고 export const viewport = mobileViewport;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. **앱 화면은 반드시 이 트리로 감싼다.** `AppShell`만 쓰면 화면 좌측에 붙고 휑하다. 중앙정렬·풀화면 배경·powered-by 풋터는 `Watermark`가 담당한다.
|
|
22
|
+
```tsx
|
|
23
|
+
"use client";
|
|
24
|
+
import { Watermark, AppShell, AppShellHeader, AppShellContent, TabBar, Tab } from "@m1kapp/kit";
|
|
25
|
+
|
|
26
|
+
<Watermark color="#0a0d16" text="앱이름" maxWidth={460}>
|
|
27
|
+
<AppShell maxWidth={460}>
|
|
28
|
+
<AppShellHeader>…헤더…</AppShellHeader>
|
|
29
|
+
<AppShellContent>…스크롤 본문…</AppShellContent>
|
|
30
|
+
<TabBar>
|
|
31
|
+
<Tab active={…} onClick={…} label="홈" icon={<span>🏠</span>} activeColor="[테마컬러]" />
|
|
32
|
+
</TabBar>
|
|
33
|
+
</AppShell>
|
|
34
|
+
</Watermark>
|
|
35
|
+
```
|
|
36
|
+
- `AppShell`/`TabBar`는 인터랙티브 → 이 트리는 **`"use client"` 컴포넌트**에 둔다. 데이터는 서버 컴포넌트(page.tsx)에서 계산해 props로 내려준다.
|
|
37
|
+
- `maxWidth`는 Watermark와 AppShell을 **같은 값**으로 맞춘다(기본 430~460).
|
|
38
|
+
|
|
39
|
+
3. **이모지는 토스페이스로 통일한다.** 장식용 이모지는 남발하지 말고, 쓰는 이모지는 토스 스타일로 폴백시킨다.
|
|
40
|
+
- layout `<head>`에 `<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/toss/tossface/dist/tossface.css" />`
|
|
41
|
+
- `globals.css` 폰트 스택에 `"Tossface"`를 본문/디스플레이 폰트 **뒤에** 끼우면 이모지 글자만 자동 폴백된다.
|
|
42
|
+
|
|
43
|
+
4. **🚫 `node_modules/@m1kapp/kit` 디렉토리 안에서 `npm run build`/스크립트를 절대 돌리지 마라.** kit 자체 빌드가 돌아 `dist/`를 날린다. 깨지면 `rm -rf node_modules/@m1kapp/kit && npm install @m1kapp/kit`로 복구. 빌드는 항상 프로젝트 루트에서.
|
|
44
|
+
|
|
45
|
+
5. **kit 컴포넌트 카탈로그**(필요 시 골라 쓰기): 레이아웃 `AppShell/AppShellHeader/AppShellContent`, 내비 `TabBar/Tab/Fab`, 표시 `Section/SectionHeader/StatChip/Badge/Avatar/EmptyState/Divider`, 브랜딩 `Watermark`, 입력 `Button/EmojiButton`, 테마 `ThemeButton/ThemeDialog`, 모션 `Typewriter`. SEO는 `@m1kapp/kit/seo`, OG는 `@m1kapp/kit/ogimage`, PWA는 `@m1kapp/kit/pwa`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
8
49
|
## Step 0: 프로젝트 파악
|
|
9
50
|
|
|
10
51
|
먼저 조용히 다음을 확인한다 (사용자에게 보고하지 않음):
|
|
11
52
|
- `package.json` — 프레임워크, @m1kapp/kit 버전
|
|
12
|
-
- `app/layout.tsx` — 기존 metadata 여부
|
|
13
|
-
- `app/
|
|
53
|
+
- `app/layout.tsx` — 기존 metadata / styles.css import / KitStyles 여부
|
|
54
|
+
- `app/page.tsx` — 이미 앱쉘로 감겨 있는지
|
|
55
|
+
- `app/sitemap.ts`, `app/robots.ts`, `app/manifest.ts`, `app/og/route.tsx` 존재 여부
|
|
14
56
|
- `public/` — 파비콘, OG 이미지 여부
|
|
15
|
-
- `tailwind.config.*` — 테마
|
|
57
|
+
- `globals.css` / `tailwind.config.*` — 테마 컬러, 폰트 스택 여부
|
|
16
58
|
|
|
17
59
|
파악이 끝나면 아래 질문들을 **한 번에 모아서** 사용자에게 묻는다.
|
|
18
60
|
|
|
@@ -26,134 +68,108 @@ description: "@m1kapp/kit 기반 Next.js 프로젝트 초기 설정을 인터랙
|
|
|
26
68
|
@m1kapp/kit 초기 설정을 시작할게요. 몇 가지만 확인할게요!
|
|
27
69
|
|
|
28
70
|
1. 앱 이름이 뭔가요?
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
(1) 커머스/쇼핑
|
|
42
|
-
(2) 블로그/콘텐츠
|
|
43
|
-
(3) 대시보드/툴
|
|
44
|
-
(4) 소셜/커뮤니티
|
|
45
|
-
(5) 랜딩/홍보
|
|
46
|
-
(6) 기타
|
|
47
|
-
|
|
48
|
-
6. 다음 중 적용할 것을 골라주세요 (복수 선택, 예: 1 2 3)
|
|
49
|
-
(1) SEO — metadata, sitemap, robots, JSON-LD
|
|
50
|
-
(2) PWA — manifest, 설치 유도 버튼, 아이콘
|
|
51
|
-
(3) OG 이미지 — /og 라우트 자동 생성
|
|
52
|
-
(4) 파비콘 — 자동 생성 (m1kkit 필요)
|
|
71
|
+
2. 메인 테마 컬러가 있나요? (hex. 없으면 #3B82F6)
|
|
72
|
+
3. 앱 한 줄 설명이 뭔가요? (메타 description + OG)
|
|
73
|
+
4. 배포 URL이 있나요? (예: https://myapp.com)
|
|
74
|
+
5. 앱 유형은? (1)커머스 (2)블로그 (3)대시보드/툴 (4)소셜 (5)랜딩 (6)기타
|
|
75
|
+
|
|
76
|
+
6. 적용할 것을 골라주세요 (복수 선택)
|
|
77
|
+
(1) 앱쉘 레이아웃 — Watermark + AppShell + 헤더 + TabBar ← 추천 (앱의 뼈대)
|
|
78
|
+
(2) Tossface 이모지 — 토스 스타일 이모지 폰트 적용
|
|
79
|
+
(3) SEO — metadata, sitemap, robots, JSON-LD
|
|
80
|
+
(4) PWA — manifest, 설치 유도 버튼, 아이콘
|
|
81
|
+
(5) OG 이미지 — /og 라우트 자동 생성
|
|
82
|
+
(6) 파비콘 — 자동 생성 (m1kkit 필요)
|
|
53
83
|
```
|
|
54
84
|
|
|
85
|
+
기본 추천은 **(1)(2)(3)(5)(6) 전체** — 앱쉘 없이 SEO만 깔면 "kit으로 만들었는데 화면이 휑하다"는 결과가 된다.
|
|
86
|
+
|
|
55
87
|
---
|
|
56
88
|
|
|
57
89
|
## Step 2: 적용 계획 출력
|
|
58
90
|
|
|
59
|
-
답변을 받으면 아래 형식으로 적용 계획을 보여준다:
|
|
60
|
-
|
|
61
91
|
```
|
|
62
92
|
✓ 확인했어요! 이렇게 적용할게요:
|
|
63
93
|
|
|
64
|
-
앱 이름:
|
|
65
|
-
테마 컬러: [색상] ████
|
|
66
|
-
설명: [한 줄 설명]
|
|
67
|
-
배포 URL: [URL]
|
|
94
|
+
앱 이름: [이름] 테마: [색] ████ 배포: [URL]
|
|
68
95
|
|
|
69
96
|
적용 항목:
|
|
70
|
-
☐ app/layout.tsx
|
|
71
|
-
☐
|
|
72
|
-
☐ app/
|
|
73
|
-
☐ app/
|
|
74
|
-
☐ app/manifest.ts
|
|
75
|
-
☐ tailwind.config — 테마 컬러 등록
|
|
97
|
+
☐ app/layout.tsx — styles.css/KitStyles/Tossface import, metadata, viewport
|
|
98
|
+
☐ components/AppRoot.tsx — Watermark>AppShell>Header/Content/TabBar ("use client")
|
|
99
|
+
☐ app/page.tsx — 데이터 계산 후 <AppRoot/> 렌더
|
|
100
|
+
☐ app/globals.css — 테마 컬러 + Tossface 폰트 스택
|
|
101
|
+
☐ app/sitemap.ts / robots.ts / manifest.ts / og/route.tsx
|
|
76
102
|
|
|
77
103
|
시작할까요? (y/n)
|
|
78
104
|
```
|
|
79
105
|
|
|
80
|
-
y면 Step 3
|
|
106
|
+
y면 Step 3, n이면 수정할 항목 다시 묻기.
|
|
81
107
|
|
|
82
108
|
---
|
|
83
109
|
|
|
84
110
|
## Step 3: 파일 생성/수정
|
|
85
111
|
|
|
86
|
-
|
|
87
|
-
각 파일 완료 시 체크 표시:
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
✓ app/layout.tsx 완료
|
|
91
|
-
✓ app/sitemap.ts 완료
|
|
92
|
-
✓ app/robots.ts 완료
|
|
93
|
-
...
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### layout.tsx 적용 규칙
|
|
97
|
-
- 기존 파일이 있으면 `metadata` export만 교체, 나머지는 보존
|
|
98
|
-
- `@m1kapp/kit/seo`의 `createMetadata`, `titleTemplate` 사용
|
|
99
|
-
- 앱 유형에 따라 적절한 JSON-LD도 추가 (WebSite, Organization 등)
|
|
112
|
+
선택한 항목을 순서대로 적용하고, 각 파일 완료 시 `✓ 파일명 완료` 표시.
|
|
100
113
|
|
|
114
|
+
### layout.tsx — (모든 프로젝트 공통, 항상 적용)
|
|
101
115
|
```ts
|
|
102
|
-
import
|
|
116
|
+
import "@m1kapp/kit/styles.css"; // ★ 필수
|
|
117
|
+
import { createMetadata, titleTemplate } from "@m1kapp/kit/seo";
|
|
118
|
+
import { KitStyles, mobileViewport } from "@m1kapp/kit/pwa";
|
|
119
|
+
import "./globals.css";
|
|
103
120
|
|
|
104
121
|
export const metadata = createMetadata({
|
|
105
|
-
title: "[앱 이름]",
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
title: "[앱 이름]", description: "[설명]", url: "[URL]",
|
|
123
|
+
siteName: "[앱 이름]", image: "[URL]/og",
|
|
124
|
+
});
|
|
125
|
+
export const viewport = mobileViewport;
|
|
126
|
+
// <head>에 <KitStyles /> + (Tossface 선택 시) tossface <link>
|
|
127
|
+
// JSON-LD는 앱 유형에 맞게 jsonLd.website / jsonLd.organization 추가
|
|
111
128
|
```
|
|
129
|
+
- 기존 파일이 있으면 import/exports만 보강하고 나머지 구조는 보존.
|
|
112
130
|
|
|
113
|
-
###
|
|
114
|
-
-
|
|
115
|
-
- `
|
|
116
|
-
-
|
|
131
|
+
### 앱쉘 레이아웃 — (1) 선택 시
|
|
132
|
+
- `"use client"` 컴포넌트(예: `components/AppRoot.tsx`)에 위 **핵심 규칙 2번** 트리를 그대로 생성.
|
|
133
|
+
- `page.tsx`(서버 컴포넌트)는 데이터만 계산해서 `<AppRoot data={…} />`로 내려준다.
|
|
134
|
+
- 탭이 여러 개면 `useState`로 탭 전환, 각 탭은 별도 패널 컴포넌트로 분리.
|
|
135
|
+
- `AppShellContent` 안의 섹션은 `Section`/`SectionHeader`/`StatChip` 등 kit 컴포넌트를 우선 사용.
|
|
117
136
|
|
|
118
|
-
###
|
|
119
|
-
-
|
|
120
|
-
-
|
|
137
|
+
### Tossface 이모지 — (2) 선택 시
|
|
138
|
+
- layout `<head>`: `<link rel="preconnect" href="https://cdn.jsdelivr.net" />` + tossface css `<link>`.
|
|
139
|
+
- `globals.css`: 본문/디스플레이 폰트 스택 끝에 `"Tossface"` 추가. 강제용 `.emoji { font-family:"Tossface"; font-style:normal; line-height:1; }`도 정의.
|
|
121
140
|
|
|
122
|
-
###
|
|
123
|
-
- `
|
|
124
|
-
-
|
|
125
|
-
- 앱 이름 + 테마 컬러 반영
|
|
141
|
+
### sitemap.ts / robots.ts — (3)
|
|
142
|
+
- `nextSitemap("[URL]", [{ path:"/", priority:1 }, …])`, `nextRobots({ disallow:["/api","/admin"], sitemap:"[URL]/sitemap.xml" })`.
|
|
143
|
+
- `app/` 하위 `page.tsx`를 스캔해 경로 자동 추출, 동적 라우트는 TODO 주석.
|
|
126
144
|
|
|
127
|
-
###
|
|
128
|
-
- `app/
|
|
129
|
-
-
|
|
130
|
-
- 테마 컬러 자동 반영
|
|
145
|
+
### OG 이미지 — (5)
|
|
146
|
+
- `app/og/route.tsx`에서 `OGImage`(`@m1kapp/kit/ogimage`) + `loadPretendard()`로 `ImageResponse` 반환.
|
|
147
|
+
- 템플릿: 기본 `default`, 대결/스포츠성은 `match`(home/away), 통계는 `stat` 등 앱 성격에 맞게.
|
|
131
148
|
|
|
132
|
-
###
|
|
133
|
-
- `
|
|
134
|
-
|
|
149
|
+
### PWA manifest — (4)
|
|
150
|
+
- `app/manifest.ts`: `createManifest({ name, themeColor:"[색]", icon:{ text:"[약자]" } })` (default export).
|
|
151
|
+
|
|
152
|
+
### globals.css 테마 컬러
|
|
153
|
+
- Tailwind v4면 `@theme inline`에 `--color-primary: var(--primary)`, `:root`에 `--primary:[hex]`. v3면 `tailwind.config`.
|
|
135
154
|
|
|
136
155
|
---
|
|
137
156
|
|
|
138
|
-
## Step 4: 완료 요약
|
|
157
|
+
## Step 4: 완료 요약 + 검증
|
|
139
158
|
|
|
140
159
|
```
|
|
141
|
-
🎉 설정 완료!
|
|
142
|
-
|
|
143
|
-
적용된 파일:
|
|
144
|
-
✓ app/layout.tsx
|
|
145
|
-
✓ app/sitemap.ts
|
|
146
|
-
...
|
|
160
|
+
🎉 설정 완료! 적용: layout / AppRoot / page / globals / sitemap / robots / manifest / og
|
|
147
161
|
|
|
148
162
|
다음 단계:
|
|
149
|
-
→
|
|
150
|
-
→
|
|
163
|
+
→ npm run build 로 타입/라우트 검증 (반드시 프로젝트 루트에서!)
|
|
164
|
+
→ npm run dev 로 앱쉘·카운트다운 등 실제 화면 확인
|
|
151
165
|
→ npx m1kkit favicon 으로 파비콘 생성
|
|
166
|
+
→ 배포 후 search.google.com/search-console 등록
|
|
152
167
|
```
|
|
168
|
+
- 마무리로 **프로젝트 루트에서** `npm run build`를 돌려 타입 에러/모듈 누락이 없는지 확인하고 결과를 보고한다.
|
|
153
169
|
|
|
154
170
|
---
|
|
155
171
|
|
|
156
172
|
## 주의사항
|
|
157
|
-
- 기존 코드를 덮어쓸 때는
|
|
158
|
-
- TypeScript 타입
|
|
159
|
-
-
|
|
173
|
+
- 기존 코드를 덮어쓸 때는 원본을 보존하며 필요한 부분만 추가/수정. 파일이 이미 있으면 사용자에게 알리고 확인.
|
|
174
|
+
- TypeScript 타입 에러 없이 마치고, 가능하면 빌드까지 통과시킨 뒤 보고.
|
|
175
|
+
- `styles.css` import 누락 / `Watermark` 미적용 / 빌드를 kit 디렉토리에서 돌리는 것 — 이 셋이 가장 흔한 실패 원인이다. (위 핵심 규칙 참조)
|