@designbasekorea/figma-license-core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -12
- package/dist/designbaseAccountAuth.d.ts +65 -0
- package/dist/designbaseAccountAuth.d.ts.map +1 -0
- package/dist/fetchLicenseTransport.d.ts +8 -0
- package/dist/fetchLicenseTransport.d.ts.map +1 -1
- package/dist/figmaStatusManager.d.ts +13 -0
- package/dist/figmaStatusManager.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +826 -59
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +829 -58
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +829 -58
- package/dist/index.umd.js.map +1 -1
- package/dist/licenseCore.d.ts +16 -5
- package/dist/licenseCore.d.ts.map +1 -1
- package/dist/licenseErrorMapper.d.ts.map +1 -1
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/PLUGIN_INTEGRATION_KO.md +331 -0
- package/package.json +5 -4
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Figma 플러그인 적용 가이드 (Korean)
|
|
2
|
+
|
|
3
|
+
이 문서는 `@designbasekorea/figma-license-core`를 실제 플러그인(`controller.ts`)에 붙일 때의 기준 문서입니다.
|
|
4
|
+
|
|
5
|
+
## 1) 해결하는 문제
|
|
6
|
+
|
|
7
|
+
- 라이선스 등록 여부 통일
|
|
8
|
+
- 키가 있는지 (`hasLicenseKey`)
|
|
9
|
+
- 실제 유효 등록인지 (`isLicenseRegistered`)
|
|
10
|
+
- 유료/무료 티어별 사용 범위 통일
|
|
11
|
+
- 일일 N회, 주간 N회, 월간 N회, 무제한
|
|
12
|
+
- 티어별 기능 제한 통일
|
|
13
|
+
- 예: 무료는 무제한 사용 가능하지만 일부 기능 차단
|
|
14
|
+
|
|
15
|
+
## 2) 기본 연결 패턴
|
|
16
|
+
|
|
17
|
+
`code.ts` 또는 `controller.ts`에서 매니저 생성:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createFigmaLicenseStatusManager } from '@designbasekorea/figma-license-core';
|
|
21
|
+
|
|
22
|
+
const licenseManager = createFigmaLicenseStatusManager({
|
|
23
|
+
config: {
|
|
24
|
+
productId: 'korean-dummy-plugin',
|
|
25
|
+
endpoint: 'https://api.example.com/license/verify',
|
|
26
|
+
deactivateEndpoint: 'https://api.example.com/license/deactivate',
|
|
27
|
+
instanceName: 'korean-dummy-plugin',
|
|
28
|
+
storagePrefix: 'korean-dummy-plugin-license',
|
|
29
|
+
policy: {
|
|
30
|
+
freeTier: 'FREE',
|
|
31
|
+
paidTier: 'PRO',
|
|
32
|
+
usageByTier: {
|
|
33
|
+
FREE: { limit: 10, reset: 'daily' },
|
|
34
|
+
PRO: { limit: 20, reset: 'monthly' },
|
|
35
|
+
},
|
|
36
|
+
featuresByTier: {
|
|
37
|
+
FREE: { deny: ['bulk_apply', 'multi_instance_apply'] },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
licenseManager.bindToOnMessage(async (msg) => {
|
|
44
|
+
// 기존 controller switch 로직을 fallback으로 유지
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 2-1) 디자인베이스 계정/멤버십 기반 연결 패턴
|
|
49
|
+
|
|
50
|
+
디자인베이스 LMS의 멤버십/상품 결제 권한으로 피그마 플러그인을 열려면 `DesignbaseAccountAuthClient`와
|
|
51
|
+
`DesignbaseAccountLicenseTransport`를 같이 사용합니다.
|
|
52
|
+
|
|
53
|
+
흐름:
|
|
54
|
+
|
|
55
|
+
1. UI에서 "디자인베이스로 로그인/결제" 버튼 클릭
|
|
56
|
+
2. 플러그인 main이 `authClient.start()` 호출
|
|
57
|
+
3. 반환된 `authUrl`을 `figma.openExternal(authUrl)`로 열기
|
|
58
|
+
4. 플러그인은 `authClient.poll(sessionId, pollToken)`을 반복 호출
|
|
59
|
+
5. 사용자가 사이트에서 로그인/멤버십/결제를 완료하면 poll 결과가 `active`가 됨
|
|
60
|
+
6. `authClient.saveTokens(result)` 후 `licenseCore.verifyLicense(result.licenseKey)` 호출
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import {
|
|
64
|
+
DesignbaseAccountAuthClient,
|
|
65
|
+
DesignbaseAccountLicenseTransport,
|
|
66
|
+
LicenseCore,
|
|
67
|
+
FigmaStorageAdapter,
|
|
68
|
+
FigmaNotifierAdapter,
|
|
69
|
+
} from '@designbasekorea/figma-license-core';
|
|
70
|
+
|
|
71
|
+
const storage = new FigmaStorageAdapter(figma);
|
|
72
|
+
const notifier = new FigmaNotifierAdapter(figma);
|
|
73
|
+
|
|
74
|
+
const authClient = new DesignbaseAccountAuthClient({
|
|
75
|
+
baseUrl: 'https://designbase.co.kr',
|
|
76
|
+
storage,
|
|
77
|
+
storagePrefix: 'korean-dummy-plugin-account',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const transport = new DesignbaseAccountLicenseTransport({
|
|
81
|
+
baseUrl: 'https://designbase.co.kr',
|
|
82
|
+
storage,
|
|
83
|
+
storagePrefix: 'korean-dummy-plugin-account',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const licenseCore = new LicenseCore(
|
|
87
|
+
{
|
|
88
|
+
productId: 'korean-dummy-plugin',
|
|
89
|
+
endpoint: 'https://designbase.co.kr/wp-json/custom/v1/product/status',
|
|
90
|
+
instanceName: 'korean-dummy-plugin',
|
|
91
|
+
storagePrefix: 'korean-dummy-plugin-license',
|
|
92
|
+
policy: {
|
|
93
|
+
freeTier: 'FREE',
|
|
94
|
+
paidTier: 'PRO',
|
|
95
|
+
usageByTier: {
|
|
96
|
+
FREE: { limit: 10, reset: 'daily' },
|
|
97
|
+
PRO: { limit: 'unlimited', reset: 'none' },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{ storage, transport, notifier },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
async function startDesignbaseAccountAuth() {
|
|
105
|
+
const deviceId = await getOrCreateDeviceId();
|
|
106
|
+
const started = await authClient.start({
|
|
107
|
+
pluginCode: 'korean-dummy-plugin',
|
|
108
|
+
deviceId,
|
|
109
|
+
returnUrl: 'https://www.figma.com/community/plugin/YOUR_PLUGIN_ID',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
figma.openExternal(started.authUrl);
|
|
113
|
+
|
|
114
|
+
const expiresAt = Date.now() + started.expiresIn * 1000;
|
|
115
|
+
while (Date.now() < expiresAt) {
|
|
116
|
+
await wait(started.pollInterval * 1000);
|
|
117
|
+
const result = await authClient.poll(started.sessionId, started.pollToken);
|
|
118
|
+
|
|
119
|
+
if (result.status === 'active' && result.licenseKey) {
|
|
120
|
+
await authClient.saveTokens(result);
|
|
121
|
+
await licenseCore.verifyLicense(result.licenseKey);
|
|
122
|
+
figma.notify('디자인베이스 계정 권한이 확인되었습니다.');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (['expired', 'access_denied'].includes(result.status)) {
|
|
127
|
+
throw new Error(result.message || '디자인베이스 인증에 실패했습니다.');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error('인증 시간이 만료되었습니다. 다시 시도해 주세요.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function getOrCreateDeviceId(): Promise<string> {
|
|
135
|
+
const stored = await figma.clientStorage.getAsync('designbase-device-id');
|
|
136
|
+
if (typeof stored === 'string' && stored) {
|
|
137
|
+
return stored;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const id = `figma-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
141
|
+
await figma.clientStorage.setAsync('designbase-device-id', id);
|
|
142
|
+
return id;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function wait(ms: number): Promise<void> {
|
|
146
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
LMS 서버 엔드포인트:
|
|
151
|
+
|
|
152
|
+
- `POST /wp-json/custom/v1/figma/auth/start`
|
|
153
|
+
- `POST /wp-json/custom/v1/figma/auth/poll`
|
|
154
|
+
- `GET /wp-json/custom/v1/figma/auth/complete`
|
|
155
|
+
- 이후 검증: `GET /wp-json/custom/v1/product/status?plugin_code=...` with `Authorization: Bearer ...`
|
|
156
|
+
|
|
157
|
+
보안 기준:
|
|
158
|
+
|
|
159
|
+
- `start`에서 생성되는 complete 토큰과 poll 토큰은 서로 다릅니다.
|
|
160
|
+
- 서버에는 원본 토큰을 저장하지 않고 SHA-256 해시만 저장합니다.
|
|
161
|
+
- 세션은 1시간 후 만료됩니다.
|
|
162
|
+
- poll 토큰 없이는 계정 토큰을 받을 수 없습니다.
|
|
163
|
+
- `returnUrl`은 `figma.com` 또는 `figma://`만 허용합니다.
|
|
164
|
+
|
|
165
|
+
## 2-2) PageLicense UI 메시지 (figma-ui)
|
|
166
|
+
|
|
167
|
+
`@designbasekorea/figma-ui`의 `PageLicense`는 `figma-license-core`와 동일한 메시지 타입을 사용합니다.
|
|
168
|
+
|
|
169
|
+
UI → main 추가 메시지:
|
|
170
|
+
|
|
171
|
+
- `start-designbase-auth` — WordPress 계정 로그인/멤버십 인증 시작 (`DesignbaseAccountAuthClient.start` 연동)
|
|
172
|
+
- `open-external-url` — `{ url }` 외부 브라우저 열기 (`figma.openExternal` 권장)
|
|
173
|
+
|
|
174
|
+
main → UI 추가 메시지:
|
|
175
|
+
|
|
176
|
+
- `designbase-auth-result` — `{ success, message, ...statusFields }` 계정 인증 결과
|
|
177
|
+
|
|
178
|
+
`update-plugin-status` 페이로드는 `FigmaLicenseStatusManager.postStatusUpdate()`와 동일합니다.
|
|
179
|
+
|
|
180
|
+
## 2-3) YouTube 멤버십 All-Access UI
|
|
181
|
+
|
|
182
|
+
상품별 가격표가 필요 없는 플러그인은 `PageLicense`를 멤버십 모드로 사용합니다. 이 모드에서는 가격, 달러 금액,
|
|
183
|
+
Lifetime 결제 문구를 렌더링하지 않고 웹사이트 랜딩 페이지와 월간 키 입력만 제공합니다.
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { PageLicense } from '@designbasekorea/figma-ui';
|
|
187
|
+
|
|
188
|
+
<PageLicense
|
|
189
|
+
status="FREE"
|
|
190
|
+
mode="youtube-membership"
|
|
191
|
+
membershipUrl="https://designbase.co.kr/membership/"
|
|
192
|
+
enableAccountAuth={false}
|
|
193
|
+
onClose={() => setShowLicense(false)}
|
|
194
|
+
/>;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
활성화된 경우 공통 상태 카드가 `source`, `access scope`, `expires at`, `last checked`를 표시합니다. 키는 기본적으로
|
|
198
|
+
마스킹되며, 서버가 최종 권한을 결정합니다. 플러그인 실행 시 `initialize()`가 서버 검증을 완료할 때까지 유료 상태를
|
|
199
|
+
신뢰하지 않으며, 만료·철회·네트워크·요청 제한 오류는 무료 상태로 전환해 기능을 잠급니다.
|
|
200
|
+
|
|
201
|
+
공통 멤버십 UI는 `LicenseOffer`, `LicenseStatusCard`로도 개별 조합할 수 있으므로, 플러그인별 화면을 만들 때 라이선스
|
|
202
|
+
검증·표시 코드를 복제하지 않습니다.
|
|
203
|
+
|
|
204
|
+
## 3) UI 표기 데이터
|
|
205
|
+
|
|
206
|
+
`update-plugin-status` 메시지에 아래 값들이 포함됩니다.
|
|
207
|
+
|
|
208
|
+
- `hasLicenseKey`
|
|
209
|
+
- `isLicenseRegistered`
|
|
210
|
+
- `paymentStatus`
|
|
211
|
+
- `planTier`
|
|
212
|
+
- `remainingUsage`
|
|
213
|
+
- `usageUsed`
|
|
214
|
+
- `usageReset` (`none`/`daily`/`weekly`/`monthly`)
|
|
215
|
+
- `usageReason` (`USAGE_LIMIT_REACHED`/`FEATURE_NOT_ALLOWED`)
|
|
216
|
+
- `licenseStatus`
|
|
217
|
+
- `licenseSource`
|
|
218
|
+
- `accessScope`
|
|
219
|
+
- `expiresAt`
|
|
220
|
+
- `lastVerifiedAt`
|
|
221
|
+
- `validationMessage`
|
|
222
|
+
|
|
223
|
+
UI 예시:
|
|
224
|
+
|
|
225
|
+
- FREE + daily + 10회 정책: "오늘 남은 횟수 6/10"
|
|
226
|
+
- FREE + weekly + 30회 정책: "이번 주 남은 횟수 18/30"
|
|
227
|
+
- PRO + monthly + 20회 정책: "이번 달 남은 횟수 12/20"
|
|
228
|
+
- FREE + unlimited + feature deny: "사용 횟수 제한 없음, 고급 기능 잠김"
|
|
229
|
+
|
|
230
|
+
## 4) 기능별 제한 체크
|
|
231
|
+
|
|
232
|
+
사용 전에 기능 접근 가능 여부 확인:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
parent.postMessage({ pluginMessage: { type: 'check-feature-access', featureKey: 'bulk_apply' } }, '*');
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
응답: `feature-access-result`
|
|
239
|
+
|
|
240
|
+
- `allowed === false` 이고 `reason === 'FEATURE_NOT_ALLOWED'` 이면 기능 차단
|
|
241
|
+
- `allowed === false` 이고 `reason === 'USAGE_LIMIT_REACHED'` 이면 업그레이드/리셋 안내
|
|
242
|
+
|
|
243
|
+
## 5) 사용량 소모 처리
|
|
244
|
+
|
|
245
|
+
기능 실행 전 접근 가능 여부를 확인하고, 실제 작업을 시작하기 직전에 사용량을 소모합니다.
|
|
246
|
+
`amount`가 남은 수량보다 크면 코어가 소비하지 않습니다. 소비된 시도는 작업이
|
|
247
|
+
외부 API 오류로 실패하더라도 재시도로 한도를 우회하지 못하도록 사용량에 포함됩니다.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
parent.postMessage({ pluginMessage: { type: 'consume-usage', amount: 1, featureKey: 'bulk_apply' } }, '*');
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
응답: `usage-consume-result`
|
|
254
|
+
|
|
255
|
+
- `success`가 `true`면 소모 완료. 마지막 1회 소비도 `success: true`, `remaining: 0`입니다.
|
|
256
|
+
- `success`가 `false`면 차단 상태
|
|
257
|
+
|
|
258
|
+
## 6) 정책 샘플
|
|
259
|
+
|
|
260
|
+
### A. 무료 일 10회, 프로 월 20회
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
policy: {
|
|
264
|
+
freeTier: 'FREE',
|
|
265
|
+
paidTier: 'PRO',
|
|
266
|
+
usageByTier: {
|
|
267
|
+
FREE: { limit: 10, reset: 'daily' },
|
|
268
|
+
PRO: { limit: 20, reset: 'monthly' },
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### A-1. 무료 주 30회
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
policy: {
|
|
277
|
+
usageByTier: {
|
|
278
|
+
FREE: { limit: 30, reset: 'weekly' },
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### B. 무료 무제한 + 일부 기능 제한
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
policy: {
|
|
287
|
+
freeTier: 'FREE',
|
|
288
|
+
usageByTier: {
|
|
289
|
+
FREE: { limit: 'unlimited', reset: 'none' },
|
|
290
|
+
},
|
|
291
|
+
featuresByTier: {
|
|
292
|
+
FREE: { deny: ['bulk_apply', 'advanced_batch'] },
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### C. 엔터프라이즈 완전 무제한
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
policy: {
|
|
301
|
+
usageByTier: {
|
|
302
|
+
ENTERPRISE: { limit: 'unlimited', reset: 'none' },
|
|
303
|
+
},
|
|
304
|
+
featuresByTier: {
|
|
305
|
+
ENTERPRISE: { defaultAllowed: true },
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## 7) 기존 controller.ts 마이그레이션 팁
|
|
311
|
+
|
|
312
|
+
- 기존 `StatusManager` 인터페이스를 즉시 제거하지 말고, 1차로 호환 어댑터를 두세요.
|
|
313
|
+
- `decrementUsageCount`는 내부적으로 `consumeUsage` 또는 `consume-usage`로 연결하세요.
|
|
314
|
+
기존 `incrementUsage`는 호환용 저수준 API로만 남기고 새 기능에서는 사용하지 마세요.
|
|
315
|
+
- UI에서 `usageCount`만 보던 경우, 앞으로는 `remainingUsage` + `usageReset` 조합으로 표기하세요.
|
|
316
|
+
|
|
317
|
+
## 8) 운영 체크리스트
|
|
318
|
+
|
|
319
|
+
- 운영 환경에서 `masterCode` 제거
|
|
320
|
+
- 서버에서 라이선스 최종 검증
|
|
321
|
+
- 클라이언트 상태 변조 대비
|
|
322
|
+
- 실패 메시지(`errorCode`)를 UI에 그대로 매핑하여 대응
|
|
323
|
+
|
|
324
|
+
## 9) 여러 플러그인에 일괄 적용하는 운영 규칙
|
|
325
|
+
|
|
326
|
+
- 각 플러그인에는 라이선스 검증·상태 카드·멤버십 안내 코드를 복사하지 않고 `@designbasekorea/figma-license-core`와 `@designbasekorea/figma-ui`만 의존합니다.
|
|
327
|
+
- 라이브러리 변경은 Changeset으로 `figma-license-core`와 `figma-ui` 버전을 함께 올린 뒤 패키지를 배포합니다.
|
|
328
|
+
- 플러그인은 테스트 단계에서 새 버전을 정확히 고정하고, Face Dummy에서 활성·만료·철회·요청 제한·네트워크 실패를 확인한 다음 다른 플러그인으로 확장합니다.
|
|
329
|
+
- 운영 플러그인에 `masterCode`를 넣지 않고, WordPress 서버를 권한의 최종 원천으로 유지합니다.
|
|
330
|
+
- 무료 한도와 차단 기능은 각 플러그인의 임의 상태 관리자가 아니라 `figma-license-core`의
|
|
331
|
+
`policy.usageByTier`와 `policy.featuresByTier`에서 선언합니다.
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designbasekorea/figma-license-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Figma plugin license core and status manager for reusable license workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"docs"
|
|
10
11
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"build": "rollup -c",
|
|
@@ -20,12 +21,12 @@
|
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@rollup/plugin-commonjs": "^25.0.4",
|
|
22
23
|
"@rollup/plugin-node-resolve": "^15.2.1",
|
|
23
|
-
"@rollup/plugin-typescript": "^
|
|
24
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
24
25
|
"@types/jest": "^29.5.5",
|
|
25
26
|
"jest": "^29.6.4",
|
|
26
27
|
"jest-environment-jsdom": "^29.6.4",
|
|
27
28
|
"rimraf": "^5.0.1",
|
|
28
|
-
"rollup": "^
|
|
29
|
+
"rollup": "^4.63.1",
|
|
29
30
|
"typescript": "^5.2.2"
|
|
30
31
|
},
|
|
31
32
|
"publishConfig": {
|