@designbasekorea/wordpress-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,890 @@
1
+ # `@designbasekorea/wordpress-ui` WordPress 관리자 적용 가이드
2
+
3
+ 이 문서는 `@designbasekorea/wordpress-ui`를 WordPress 플러그인과 테마의 관리자 화면에 적용하는 표준 가이드입니다.
4
+
5
+ 이 패키지는 `@designbasekorea/ui-wc`를 내부 구현으로 사용하지만, 소비자는 `ui-wc`를 직접 import하지 않습니다. 소비자 코드는 항상 `wordpress-ui`의 공개 진입점과 배포된 자산만 사용합니다.
6
+
7
+ 현재 패키지는 다음 원칙을 따릅니다.
8
+
9
+ - 기본 런타임은 React가 없는 Web Component/vanilla 방식입니다.
10
+ - React adapter는 선택 사항입니다. React를 사용하지 않는 플러그인과 테마에는 React를 로드하지 않습니다.
11
+ - WordPress 전체 관리자 프레임을 대체하지 않습니다.
12
+ - 플러그인 또는 테마가 소유한 관리자 화면 안에만 페이지 shell을 렌더링합니다.
13
+ - UI는 표현과 상호작용만 담당하고, REST·AJAX·Settings API·권한·nonce·비즈니스 로직은 소비자가 담당합니다.
14
+ - `styles.css`는 Designbase theme token과 필요한 아이콘 폰트를 포함한 self-contained 배포물입니다.
15
+ - CDN을 사용하지 않고 플러그인 또는 테마 배포물에 JS/CSS/font를 포함합니다.
16
+
17
+ ## 1. 권장 아키텍처
18
+
19
+ ```text
20
+ WordPress plugin/theme
21
+
22
+ ├─ admin_enqueue_scripts: 현재 화면 확인 및 allowlist
23
+ ├─ REST / AJAX / Settings API: 데이터·권한·저장
24
+
25
+ └─ wordpress-ui 자산
26
+ ├─ dist/styles.css
27
+ ├─ dist/browser.iife.js
28
+ └─ dist/icons.woff2 / icons.woff
29
+
30
+ └─ .designbase-wp-admin
31
+ ├─ plugin/theme 내부 shell
32
+ ├─ native HTML controls
33
+ └─ db-* Web Components
34
+ ```
35
+
36
+ ### 1.1 WordPress 전역 관리자와 플러그인 내부 shell의 경계
37
+
38
+ `AdminShell`과 `AdminSidebar`는 플러그인 또는 테마 내부 화면을 위한 것입니다. 다음 전역 요소는 수정하지 않습니다.
39
+
40
+ - `#adminmenu`
41
+ - `#wpadminbar`
42
+ - WordPress 전역 `.wrap`의 기본 동작
43
+ - 다른 플러그인이 등록한 notice, modal, table, button
44
+ - `body`, `:root`, 전역 `button`, 전역 `table` 스타일
45
+
46
+ 각 화면의 최상위 컨테이너에 `.designbase-wp-admin`을 추가해야 합니다. 패키지 스타일은 이 root 아래로 scope되어 있으므로 root가 없으면 일부 컴포넌트가 기본 브라우저 스타일처럼 보일 수 있습니다.
47
+
48
+ ```html
49
+ <div class="wrap designbase-wp-admin myplugin-admin" data-myplugin-screen="settings">
50
+ <!-- 이 영역만 wordpress-ui가 소유한다. -->
51
+ </div>
52
+ ```
53
+
54
+ 테마 관리자도 같은 규칙을 사용합니다. 테마가 관리자를 전역적으로 꾸미는 방식이 아니라, 테마 설정 페이지의 화면 root 안에서만 사용합니다.
55
+
56
+ ## 2. 패키지 설치와 배포
57
+
58
+ ### 2.1 Node 프로젝트에 설치
59
+
60
+ ```bash
61
+ npm install @designbasekorea/wordpress-ui
62
+ ```
63
+
64
+ WordPress 서버가 npm을 실행하는 것이 아닙니다. 라이브러리를 설치한 뒤 플러그인 또는 테마 빌드 과정에서 배포에 필요한 파일을 포함해야 합니다.
65
+
66
+ 권장 배포 방식은 다음과 같습니다.
67
+
68
+ 1. 라이브러리 버전을 package lock에 고정합니다.
69
+ 2. `node_modules`에서 패키지의 `dist`와 PHP helper를 플러그인/테마의 vendor 폴더로 복사합니다.
70
+ 3. 플러그인/테마의 `admin_enqueue_scripts`에서 로컬 파일을 enqueue합니다.
71
+ 4. 최종 zip에 JS, CSS, WOFF font가 포함되었는지 확인합니다.
72
+
73
+ ### 2.2 패키지 파일 구조
74
+
75
+ `wordpress-ui` 패키지를 그대로 vendor에 포함할 경우 다음 구조를 사용합니다.
76
+
77
+ ```text
78
+ my-plugin/
79
+ ├─ my-plugin.php
80
+ ├─ vendor/
81
+ │ └─ wordpress-ui/
82
+ │ ├─ wordpress-ui.php
83
+ │ └─ dist/
84
+ │ ├─ browser.iife.js
85
+ │ ├─ styles.css
86
+ │ ├─ icons.woff2
87
+ │ ├─ icons.woff
88
+ │ └─ styles/
89
+ │ ├─ controls.css
90
+ │ ├─ overlays.css
91
+ │ └─ shell.css
92
+ └─ assets/
93
+ └─ dist/
94
+ ├─ admin.js
95
+ └─ admin.css
96
+ ```
97
+
98
+ `wordpress-ui.php`의 enqueue helper는 패키지 root를 기준으로 `dist/styles.css`와 `dist/browser.iife.js`를 찾습니다. 따라서 helper를 사용할 때는 `base_url`과 `base_path`를 `vendor/wordpress-ui`에 맞춰 전달해야 합니다.
99
+
100
+ 선택 로딩이 필요하지 않다면 `dist/styles.css` 하나만 사용합니다. 이 파일에는 테마 토큰, ui-wc 컴포넌트 스타일, 관리자 shell/overlay 스타일, 아이콘 폰트 규칙이 재조합되어 있습니다. 별도의 `theme.css`나 CDN 아이콘 CSS를 추가로 enqueue하지 않습니다.
101
+
102
+ ### 2.3 배포 전 패키지 확인
103
+
104
+ 라이브러리 저장소에서 다음을 실행합니다.
105
+
106
+ ```bash
107
+ cd packages/wordpress-ui
108
+ npm install
109
+ npm run build
110
+ npm run verify
111
+ npm run audit:axe
112
+ npm pack --dry-run
113
+ ```
114
+
115
+ `npm pack --dry-run` 결과에는 최소한 다음이 있어야 합니다.
116
+
117
+ - `dist/styles.css`
118
+ - `dist/browser.iife.js`
119
+ - `dist/icons.woff2`
120
+ - `dist/icons.woff`
121
+ - `wordpress-ui.php`
122
+ - `README.md`
123
+ - `docs/WORDPRESS-ADMIN-GUIDE.md`
124
+
125
+ 실제 플러그인/테마 배포 zip에는 `node_modules`, 소스 파일, 테스트용 서버, 로컬 설정 파일, secret을 포함하지 않습니다.
126
+
127
+ ## 3. 자산 enqueue 표준
128
+
129
+ ### 3.1 플러그인: 권장 구현
130
+
131
+ 아래 예시는 플러그인 main file 옆에 `vendor/wordpress-ui`가 있는 경우입니다.
132
+
133
+ ```php
134
+ <?php
135
+
136
+ defined('ABSPATH') || exit;
137
+
138
+ require_once __DIR__ . '/vendor/wordpress-ui/wordpress-ui.php';
139
+
140
+ final class MyPlugin_Admin_Assets
141
+ {
142
+ private const UI_VERSION = '0.1.0';
143
+
144
+ public static function init(): void
145
+ {
146
+ add_action('admin_enqueue_scripts', [self::class, 'enqueue']);
147
+ }
148
+
149
+ public static function enqueue(string $hook_suffix): void
150
+ {
151
+ $screen = function_exists('get_current_screen') ? get_current_screen() : null;
152
+ $screen_id = $screen ? $screen->id : '';
153
+
154
+ $allowed_screens = [
155
+ 'toplevel_page_myplugin',
156
+ 'myplugin_page_myplugin-settings',
157
+ ];
158
+
159
+ if (!in_array($screen_id, $allowed_screens, true)) {
160
+ return;
161
+ }
162
+
163
+ // 화면마다 필요한 capability가 다르면 화면별로 분리한다.
164
+ if (!self::can_access_screen($screen_id)) {
165
+ return;
166
+ }
167
+
168
+ $base_url = plugin_dir_url(__FILE__) . 'vendor/wordpress-ui';
169
+ $base_path = plugin_dir_path(__FILE__) . 'vendor/wordpress-ui';
170
+
171
+ designbase_wordpress_ui_enqueue([
172
+ 'handle' => 'myplugin-wordpress-ui',
173
+ 'base_url' => $base_url,
174
+ 'base_path' => $base_path,
175
+ 'version' => self::UI_VERSION,
176
+ ]);
177
+
178
+ wp_enqueue_style(
179
+ 'myplugin-admin',
180
+ plugin_dir_url(__FILE__) . 'assets/dist/admin.css',
181
+ ['myplugin-wordpress-ui'],
182
+ self::UI_VERSION
183
+ );
184
+
185
+ wp_enqueue_script(
186
+ 'myplugin-admin',
187
+ plugin_dir_url(__FILE__) . 'assets/dist/admin.js',
188
+ ['myplugin-wordpress-ui-browser'],
189
+ self::UI_VERSION,
190
+ true
191
+ );
192
+
193
+ wp_localize_script('myplugin-admin', 'MyPluginAdmin', [
194
+ 'restUrl' => esc_url_raw(rest_url('myplugin/v1')),
195
+ 'nonce' => wp_create_nonce('wp_rest'),
196
+ 'screen' => $screen_id,
197
+ ]);
198
+ }
199
+
200
+ private static function can_access_screen(string $screen_id): bool
201
+ {
202
+ if ($screen_id === 'myplugin_page_myplugin-settings') {
203
+ return current_user_can('manage_options');
204
+ }
205
+
206
+ return current_user_can('edit_posts');
207
+ }
208
+ }
209
+
210
+ MyPlugin_Admin_Assets::init();
211
+ ```
212
+
213
+ `designbase_wordpress_ui_enqueue()`는 자산을 enqueue할 뿐이며, 현재 화면이나 사용자의 capability를 확인하지 않습니다. 화면 allowlist와 권한 검사는 소비 플러그인이 반드시 수행해야 합니다.
214
+
215
+ `$hook_suffix`는 WordPress가 전달하는 값이고, `get_current_screen()->id`는 화면 식별에 더 읽기 쉬운 경우가 많습니다. 두 값을 혼동하지 말고 실제 화면에서 로그로 확인한 뒤 allowlist를 고정합니다. 일반적으로는 `screen_id` 기준 allowlist를 권장합니다.
216
+
217
+ ### 3.2 테마: 클래식 테마
218
+
219
+ 테마 관리자 페이지는 `get_template_directory_*()`를 사용합니다.
220
+
221
+ ```php
222
+ <?php
223
+
224
+ defined('ABSPATH') || exit;
225
+
226
+ require_once get_template_directory() . '/vendor/wordpress-ui/wordpress-ui.php';
227
+
228
+ add_action('admin_enqueue_scripts', static function (string $hook_suffix): void {
229
+ $screen = function_exists('get_current_screen') ? get_current_screen() : null;
230
+ if (!$screen || $screen->id !== 'appearance_page_mytheme-settings') {
231
+ return;
232
+ }
233
+
234
+ if (!current_user_can('manage_options')) {
235
+ return;
236
+ }
237
+
238
+ designbase_wordpress_ui_enqueue([
239
+ 'handle' => 'mytheme-wordpress-ui',
240
+ 'base_url' => get_template_directory_uri() . '/vendor/wordpress-ui',
241
+ 'base_path' => get_template_directory() . '/vendor/wordpress-ui',
242
+ 'version' => '1.0.0',
243
+ ]);
244
+ });
245
+ ```
246
+
247
+ 자식 테마를 지원하고 vendor 파일이 자식 테마에 포함된다면 `get_stylesheet_directory_uri()`와 `get_stylesheet_directory()`를 사용합니다. 부모 테마의 vendor를 항상 사용할 정책이면 `get_template_directory_*()`를 유지하고, 이 정책을 문서화합니다.
248
+
249
+ ### 3.3 enqueue 규칙
250
+
251
+ - `admin_enqueue_scripts`에서만 관리자 자산을 등록합니다.
252
+ - 현재 화면이 허용된 화면일 때만 `styles.css`와 `browser.iife.js`를 enqueue합니다.
253
+ - WordPress 전역 관리자 화면에 자산을 무조건 enqueue하지 않습니다.
254
+ - `browser.iife.js`는 `styles.css`보다 먼저 또는 함께 등록되어야 하며, 플러그인 JS는 `myplugin-wordpress-ui-browser`에 의존합니다.
255
+ - 버전은 패키지 버전, 플러그인 버전 또는 `filemtime()` 중 하나로 일관되게 관리합니다.
256
+ - 사용자가 접근하지 못하는 화면에 자산을 enqueue하지 않습니다. 단, 자산 노출 자체가 민감한 정보를 포함하지 않는지도 별도로 검토합니다.
257
+ - 최종 HTML에서 CDN, unpkg, jsDelivr, 외부 icon font를 요청하지 않습니다.
258
+
259
+ ## 4. HTML/vanilla 적용
260
+
261
+ WordPress 플러그인과 테마의 기본 선택지는 vanilla/PHP입니다. React가 없어도 동일한 Designbase 스타일과 ui-wc Web Component를 사용할 수 있습니다.
262
+
263
+ ### 4.1 페이지 기본 구조
264
+
265
+ ```php
266
+ <div class="wrap designbase-wp-admin myplugin-admin">
267
+ <div class="dbwp-admin-shell dbwp-admin-shell--has-sidebar">
268
+ <aside class="dbwp-admin-shell__sidebar" aria-label="My Plugin 메뉴">
269
+ <div class="dbwp-admin-sidebar">
270
+ <div class="dbwp-admin-sidebar__title">My Plugin</div>
271
+ <nav class="dbwp-admin-sidebar__nav">
272
+ <a class="dbwp-admin-sidebar__link is-active"
273
+ href="<?php echo esc_url(admin_url('admin.php?page=myplugin')); ?>"
274
+ aria-current="page">
275
+ <i class="dbwp-admin-icon icon-dashboard" aria-hidden="true"></i>
276
+ <span class="dbwp-admin-sidebar__label">대시보드</span>
277
+ </a>
278
+ <a class="dbwp-admin-sidebar__link"
279
+ href="<?php echo esc_url(admin_url('admin.php?page=myplugin-settings')); ?>">
280
+ <i class="dbwp-admin-icon icon-settings" aria-hidden="true"></i>
281
+ <span class="dbwp-admin-sidebar__label">설정</span>
282
+ </a>
283
+ </nav>
284
+ </div>
285
+ </aside>
286
+
287
+ <main class="dbwp-admin-shell__main" id="main-content">
288
+ <header class="dbwp-admin-page-header">
289
+ <div class="dbwp-admin-page-header__heading">
290
+ <i class="dbwp-admin-icon icon-dashboard" aria-hidden="true"></i>
291
+ <div>
292
+ <h1 class="dbwp-admin-page-header__title">대시보드</h1>
293
+ <p class="dbwp-admin-page-header__description">플러그인 상태와 최근 작업을 확인합니다.</p>
294
+ </div>
295
+ </div>
296
+ <div class="dbwp-admin-page-header__actions">
297
+ <a class="button button-secondary" href="<?php echo esc_url(admin_url('admin.php?page=myplugin-settings')); ?>">
298
+ 설정
299
+ </a>
300
+ </div>
301
+ </header>
302
+
303
+ <div class="dbwp-admin-page-content">
304
+ <!-- tabs, filter, section, table 등을 배치한다. -->
305
+ </div>
306
+ </main>
307
+ </div>
308
+ </div>
309
+ ```
310
+
311
+ 컴포넌트 스타일을 사용하는 자체 markup은 `.designbase-wp-admin` 내부에 둡니다. 단순한 링크와 form submit은 WordPress의 native `<a>`와 `<button>`을 우선 사용하고, 복합 동작이 필요한 경우에만 Web Component를 사용합니다.
312
+
313
+ ### 4.2 ui-wc 기본 컴포넌트 사용
314
+
315
+ `browser` entry를 로드하면 `wordpress-ui`가 관리자 화면에 필요한 ui-wc element를 등록합니다.
316
+
317
+ ```html
318
+ <form class="myplugin-filter-form" data-myplugin-filter-form>
319
+ <db-search-bar
320
+ name="search"
321
+ placeholder="제목 검색"
322
+ aria-label="제목 검색">
323
+ </db-search-bar>
324
+
325
+ <db-select
326
+ name="status"
327
+ placeholder="전체 상태"
328
+ options='[{"value":"all","label":"전체 상태"},{"value":"published","label":"공개"},{"value":"draft","label":"임시글"}]'>
329
+ </db-select>
330
+
331
+ <button type="reset" class="button button-secondary">초기화</button>
332
+ <button type="submit" class="button button-primary">적용</button>
333
+ </form>
334
+ ```
335
+
336
+ 컴포넌트의 attribute/property 계약은 패키지의 공개 타입과 Storybook 예제를 기준으로 확인합니다. HTML attribute로 JSON을 전달하는 경우 작은 데이터만 넣고, 사용자 입력을 그대로 HTML attribute에 연결하지 않습니다.
337
+
338
+ ### 4.3 vanilla 이벤트 연결
339
+
340
+ 이벤트 이름은 Web Component가 제공하는 `db-*` 이벤트를 사용합니다. 데이터 조회는 이벤트 핸들러에서 플러그인 코드로 위임합니다.
341
+
342
+ ```js
343
+ const select = document.querySelector('db-select[name="status"]');
344
+
345
+ select?.addEventListener('db-change', (event) => {
346
+ const value = event.detail?.value ?? 'all';
347
+ const url = new URL(window.location.href);
348
+
349
+ if (value === 'all') {
350
+ url.searchParams.delete('status');
351
+ } else {
352
+ url.searchParams.set('status', value);
353
+ }
354
+
355
+ window.location.assign(url.toString());
356
+ });
357
+ ```
358
+
359
+ 일반적으로 사용하는 이벤트 매핑은 다음과 같습니다.
360
+
361
+ | 목적 | 이벤트 |
362
+ | --- | --- |
363
+ | 값 변경 | `db-change` |
364
+ | text input 변경 | `db-input` |
365
+ | 검색 실행 | `db-search` |
366
+ | clear 실행 | `db-clear` |
367
+ | modal/drawer 닫힘 | `db-close` |
368
+ | confirm/cancel | `db-confirm`, `db-cancel` |
369
+ | toggle 변경 | `db-toggle` |
370
+ | page 변경 | `db-change` |
371
+
372
+ 실제 `event.detail` 모양은 사용하는 컴포넌트의 타입과 구현을 기준으로 확인합니다. 이벤트 전체를 추측해 사용하지 말고, 브라우저에서 한 번 출력해 소비 계약을 고정합니다.
373
+
374
+ ### 4.4 아이콘 사용
375
+
376
+ 아이콘은 이모지나 임의의 유니코드 문자를 사용하지 않고 Designbase icons 이름을 사용합니다.
377
+
378
+ ```html
379
+ <i class="dbwp-admin-icon icon-dashboard" aria-hidden="true"></i>
380
+ <i class="dbwp-admin-icon icon-file-manager" aria-hidden="true"></i>
381
+ <i class="dbwp-admin-icon icon-search" aria-hidden="true"></i>
382
+ <i class="dbwp-admin-icon icon-settings" aria-hidden="true"></i>
383
+ ```
384
+
385
+ `styles.css`가 아이콘 폰트와 `@font-face` 규칙을 포함합니다. 아이콘이 보이지 않으면 다음을 확인합니다.
386
+
387
+ 1. `dist/styles.css`가 실제 화면에 로드되었는지 확인합니다.
388
+ 2. `dist/icons.woff2`와 `dist/icons.woff`가 CSS가 기대하는 상대 경로에 있는지 확인합니다.
389
+ 3. 브라우저 Network에서 font 요청이 404가 아닌지 확인합니다.
390
+ 4. 임의의 외부 icon font CSS를 추가해 font-family를 덮어쓰지 않았는지 확인합니다.
391
+
392
+ ## 5. React adapter 적용
393
+
394
+ React는 선택 사항입니다. Gutenberg나 기존 React 앱과 연결해야 하는 화면에서만 사용하고, 일반적인 PHP admin page에는 vanilla entry를 권장합니다.
395
+
396
+ ```bash
397
+ npm install @designbasekorea/wordpress-ui react react-dom
398
+ ```
399
+
400
+ ```tsx
401
+ import '@designbasekorea/wordpress-ui/styles.css';
402
+ import {
403
+ AdminDataTable,
404
+ AdminEmptyState,
405
+ AdminFilterBar,
406
+ AdminPage,
407
+ AdminPageContent,
408
+ AdminPageHeader,
409
+ AdminSection,
410
+ AdminShell,
411
+ type AdminNavItem,
412
+ } from '@designbasekorea/wordpress-ui/react';
413
+
414
+ const sidebarItems: AdminNavItem[] = [
415
+ { id: 'dashboard', label: '대시보드', href: '?page=myplugin', icon: 'dashboard', active: true },
416
+ { id: 'settings', label: '설정', href: '?page=myplugin-settings', icon: 'settings' },
417
+ ];
418
+
419
+ export function MyPluginAdmin() {
420
+ return (
421
+ <div className="designbase-wp-admin">
422
+ <AdminShell sidebarItems={sidebarItems} sidebarTitle="My Plugin">
423
+ <AdminPage>
424
+ <AdminPageHeader
425
+ title="콘텐츠 관리"
426
+ icon="file-manager"
427
+ description="콘텐츠를 검색하고 상태를 관리합니다."
428
+ />
429
+ <AdminPageContent maxWidth="xl">
430
+ <AdminSection title="검색 및 필터">
431
+ <AdminFilterBar
432
+ searchPlaceholder="제목 검색"
433
+ statusPlaceholder="전체 상태"
434
+ statusOptions={[
435
+ { value: 'all', label: '전체 상태' },
436
+ { value: 'published', label: '공개' },
437
+ { value: 'draft', label: '임시글' },
438
+ ]}
439
+ onSubmit={(event) => {
440
+ event.preventDefault();
441
+ // REST 또는 URL query 변경은 소비 앱이 담당한다.
442
+ }}
443
+ />
444
+ </AdminSection>
445
+ <AdminSection title="목록">
446
+ <AdminDataTable
447
+ columns={[
448
+ { id: 'title', header: '제목', key: 'title', sortable: true },
449
+ { id: 'status', header: '상태', key: 'status' },
450
+ ]}
451
+ rows={[]}
452
+ emptyState={<AdminEmptyState title="콘텐츠가 없습니다." />}
453
+ />
454
+ </AdminSection>
455
+ </AdminPageContent>
456
+ </AdminPage>
457
+ </AdminShell>
458
+ </div>
459
+ );
460
+ }
461
+ ```
462
+
463
+ React adapter의 책임은 관리자 shell과 컴포넌트 조합입니다.
464
+
465
+ - `AdminShell`: 내부 sidebar와 main layout
466
+ - `AdminSidebar`: 메뉴, active 상태, badge, 모바일 close
467
+ - `AdminPage`, `AdminPageContent`: 페이지 폭과 밀도
468
+ - `AdminPageHeader`: title, icon, description, breadcrumb, action, back link
469
+ - `AdminSection`: 제목, 설명, action, optional collapse
470
+ - `AdminFilterBar`: 검색과 상태 필터 표현
471
+ - `AdminTabs`: 탭 표현과 active 상태
472
+ - `AdminDataTable`: column, row, sort, loading, empty, error 상태 표현
473
+ - `AdminEmptyState`, `AdminLoadingState`, `AdminErrorState`: 상태 화면
474
+
475
+ 이 컴포넌트들은 REST 요청, 페이지네이션 API, nonce 갱신, capability 검사를 대신하지 않습니다. React state와 서버 데이터의 생명주기는 플러그인/테마 앱이 소유합니다.
476
+
477
+ ## 6. REST, AJAX, Settings API 연결
478
+
479
+ ### 6.1 REST route의 기본 원칙
480
+
481
+ UI가 호출하는 endpoint는 플러그인 또는 테마가 등록합니다. 모든 route에는 명시적인 `permission_callback`이 있어야 합니다.
482
+
483
+ ```php
484
+ add_action('rest_api_init', static function (): void {
485
+ register_rest_route('myplugin/v1', '/items', [
486
+ 'methods' => WP_REST_Server::READABLE,
487
+ 'callback' => static function (WP_REST_Request $request): WP_REST_Response {
488
+ $search = sanitize_text_field((string) $request->get_param('search'));
489
+ $status = sanitize_key((string) $request->get_param('status'));
490
+
491
+ $items = MyPlugin_Items::query([
492
+ 'search' => $search,
493
+ 'status' => $status,
494
+ ]);
495
+
496
+ return rest_ensure_response([
497
+ 'items' => $items,
498
+ 'total' => count($items),
499
+ ]);
500
+ },
501
+ 'permission_callback' => static function (): bool {
502
+ return current_user_can('edit_posts');
503
+ },
504
+ 'args' => [
505
+ 'search' => [
506
+ 'sanitize_callback' => 'sanitize_text_field',
507
+ ],
508
+ 'status' => [
509
+ 'sanitize_callback' => 'sanitize_key',
510
+ 'validate_callback' => static function ($value): bool {
511
+ return in_array($value, ['all', 'published', 'draft'], true);
512
+ },
513
+ ],
514
+ ],
515
+ ]);
516
+ });
517
+ ```
518
+
519
+ 읽기 endpoint라도 capability를 검사합니다. 쓰기 endpoint는 더 강한 capability, 입력 검증, 저장 실패 처리, 감사 로그가 필요할 수 있습니다. nonce는 요청 출처를 확인하는 장치이지 capability나 인증을 대신하지 않습니다.
520
+
521
+ ### 6.2 vanilla fetch
522
+
523
+ `wp_localize_script()`로 전달한 값에는 URL, nonce, 화면 상태처럼 브라우저에 공개되어도 되는 값만 넣습니다.
524
+
525
+ ```js
526
+ async function loadItems({ search = '', status = 'all' } = {}) {
527
+ const url = new URL(`${MyPluginAdmin.restUrl}/items`);
528
+ url.searchParams.set('search', search);
529
+ url.searchParams.set('status', status);
530
+
531
+ const response = await fetch(url, {
532
+ headers: {
533
+ Accept: 'application/json',
534
+ 'X-WP-Nonce': MyPluginAdmin.nonce,
535
+ },
536
+ credentials: 'same-origin',
537
+ });
538
+
539
+ if (!response.ok) {
540
+ throw new Error(`Request failed: ${response.status}`);
541
+ }
542
+
543
+ return response.json();
544
+ }
545
+ ```
546
+
547
+ 화면에서는 로딩·성공·empty·error 상태를 각각 표현합니다. 실패를 조용히 무시하거나 `alert()`만 호출하지 않습니다.
548
+
549
+ ```js
550
+ try {
551
+ renderLoadingState();
552
+ const result = await loadItems(filters);
553
+ renderItems(result.items);
554
+ window.DesignbaseWordPressUI?.showSuccessToast('목록을 불러왔습니다.');
555
+ } catch (error) {
556
+ renderErrorState('콘텐츠를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.');
557
+ window.DesignbaseWordPressUI?.showErrorToast('요청을 처리하지 못했습니다.');
558
+ }
559
+ ```
560
+
561
+ ### 6.3 toast와 confirm
562
+
563
+ 브라우저에 `browser.iife.js`가 로드되면 다음 imperative API를 사용할 수 있습니다.
564
+
565
+ ```js
566
+ window.DesignbaseWordPressUI?.showSuccessToast('설정이 저장되었습니다.');
567
+
568
+ const confirmed = await window.DesignbaseWordPressUI?.showConfirmModal({
569
+ title: '콘텐츠를 삭제할까요?',
570
+ message: '삭제한 콘텐츠는 복구할 수 없습니다.',
571
+ confirmText: '삭제',
572
+ cancelText: '취소',
573
+ type: 'danger',
574
+ });
575
+
576
+ if (confirmed) {
577
+ await deleteItem();
578
+ }
579
+ ```
580
+
581
+ 중요한 삭제나 대량 변경에는 confirm을 사용합니다. 실제 삭제 권한과 재검증은 서버 endpoint에서 다시 수행해야 합니다.
582
+
583
+ ### 6.4 Settings API
584
+
585
+ WordPress 설정 페이지는 가능하면 Settings API를 사용합니다.
586
+
587
+ ```php
588
+ register_setting('myplugin', 'myplugin_options', [
589
+ 'type' => 'array',
590
+ 'sanitize_callback' => static function ($value): array {
591
+ return [
592
+ 'enabled' => !empty($value['enabled']),
593
+ 'label' => sanitize_text_field((string) ($value['label'] ?? '')),
594
+ ];
595
+ },
596
+ ]);
597
+ ```
598
+
599
+ 설정 form을 직접 출력할 때는 `settings_fields()`를 포함하고, 저장 action의 capability와 nonce를 함께 검증합니다. UI 패키지는 설정 저장 방식에 관여하지 않습니다.
600
+
601
+ ## 7. 화면 구성 표준
602
+
603
+ 신규 관리자 화면은 다음 순서로 구성합니다.
604
+
605
+ ```text
606
+ .designbase-wp-admin
607
+ └─ AdminShell 또는 dbwp-admin-shell
608
+ ├─ AdminSidebar 또는 plugin 내부 nav
609
+ └─ main
610
+ ├─ AdminPageHeader / db-page-header
611
+ ├─ AdminTabs / db-tabs (필요한 경우)
612
+ └─ AdminPageContent
613
+ ├─ AdminSection / db-section
614
+ ├─ AdminFilterBar
615
+ └─ AdminDataTable 또는 native table
616
+ ```
617
+
618
+ ### 7.1 페이지 헤더
619
+
620
+ 페이지 헤더에는 화면의 목적을 명확하게 표시합니다.
621
+
622
+ - `h1`은 화면마다 하나만 둡니다.
623
+ - 제목 옆 아이콘은 의미를 보조할 때만 사용합니다.
624
+ - 설명은 제목과 직접 관련된 짧은 문장으로 작성합니다.
625
+ - primary action은 우측 action 영역에 둡니다.
626
+ - 뒤로가기는 브라우저 history에 의존하지 말고 의미 있는 URL을 제공합니다.
627
+
628
+ ### 7.2 section
629
+
630
+ 서로 다른 목적의 콘텐츠를 하나의 큰 카드에 몰아넣지 않습니다. 제목·설명·action이 있는 `AdminSection` 또는 `db-section`으로 묶습니다. 접기 상태를 제공할 때에는 키보드로 열고 닫을 수 있어야 하며, 기본 상태는 콘텐츠 중요도에 따라 결정합니다.
631
+
632
+ ### 7.3 filter bar
633
+
634
+ 필터 바는 서버 query와 연결되는 명확한 상태를 가져야 합니다.
635
+
636
+ - 검색어, 상태, 날짜, taxonomy 등 필터의 현재 값을 화면에 반영합니다.
637
+ - `적용`과 `초기화`의 동작을 구분합니다.
638
+ - 서버 페이지네이션을 사용할 때 필터가 변경되면 page를 1로 되돌립니다.
639
+ - URL query에 상태를 반영하면 새로고침·뒤로가기·공유가 쉬워집니다.
640
+ - 데이터 fetching은 filter bar 안이 아니라 화면 controller에서 수행합니다.
641
+
642
+ ### 7.4 data table
643
+
644
+ 단순한 표는 native `<table>`을 우선 사용합니다. 정렬·행 action·empty/loading/error가 반복되거나 React 데이터 모델을 사용하는 경우 `AdminDataTable` 또는 `db-table`을 사용합니다.
645
+
646
+ - 열 제목을 `<th>`로 출력합니다.
647
+ - 행 action은 명확한 accessible name을 사용합니다.
648
+ - 정렬 방향은 텍스트나 `aria-sort`로 전달합니다.
649
+ - 로딩 중 기존 데이터가 있으면 전체 레이아웃을 깜빡이지 않고 해당 영역만 busy로 표시합니다.
650
+ - 데이터가 없을 때 빈 `<table>`만 보여주지 말고 `AdminEmptyState`를 제공합니다.
651
+ - 삭제·상태 변경 action은 서버 응답이 성공한 뒤 목록을 갱신합니다.
652
+
653
+ ## 8. 상태 표현과 오류 처리
654
+
655
+ 화면 controller는 최소한 다음 상태를 구분합니다.
656
+
657
+ | 상태 | 사용자에게 보여줄 것 | 권장 구현 |
658
+ | --- | --- | --- |
659
+ | loading | 작업 중임과 예상 영역 | `AdminLoadingState`, `db-spinner`, `db-skeleton` |
660
+ | empty | 결과가 없다는 설명과 다음 action | `AdminEmptyState`, `db-empty-state` |
661
+ | error | 문제와 재시도 방법 | `AdminErrorState`, `db-alert` |
662
+ | success | 저장·삭제 완료 피드백 | toast + 변경된 영역 갱신 |
663
+ | unauthorized | 권한 부족 안내 | 서버의 403/WordPress 권한 화면과 일치 |
664
+
665
+ 로딩 중인 영역에는 `aria-busy="true"`를 사용하고, 오류에는 `role="alert"` 또는 적절한 live region을 사용합니다. disabled 상태만으로 네트워크 요청을 표현하지 말고, 진행 상태도 함께 표시합니다.
666
+
667
+ ## 9. 보안 기준
668
+
669
+ `wordpress-ui`는 보안 경계를 제공하지 않습니다. 다음 항목은 각 플러그인과 테마가 구현하고 검토해야 합니다.
670
+
671
+ - 페이지 진입 시 capability를 확인합니다.
672
+ - REST route마다 `permission_callback`을 정의합니다.
673
+ - REST cookie 인증 요청에는 `X-WP-Nonce`를 사용합니다.
674
+ - AJAX 요청에는 `check_ajax_referer()`를 사용합니다.
675
+ - nonce를 인증·권한 검사의 대체 수단으로 사용하지 않습니다.
676
+ - 입력은 용도에 맞게 sanitize하고, 허용 목록·범위·타입을 validate합니다.
677
+ - 출력은 context에 맞게 `esc_html()`, `esc_attr()`, `esc_url()`, `wp_json_encode()` 등으로 escape합니다.
678
+ - SQL은 `$wpdb->prepare()`를 사용하고, sort/order/column/table 값은 allowlist로 제한합니다.
679
+ - 파일 경로·업로드·다운로드는 path traversal과 capability를 검증합니다.
680
+ - `wp_localize_script()`에 secret, private token, 전체 사용자 데이터, 내부 경로를 넣지 않습니다.
681
+ - 실패 응답에 SQL, 파일 시스템 경로, stack trace를 노출하지 않습니다.
682
+ - 개인정보를 저장한다면 보존 기간, export/erase, privacy policy를 별도로 설계합니다.
683
+
684
+ 특히 프런트엔드에서 버튼을 숨기는 것만으로 권한을 처리하면 안 됩니다. 사용자가 직접 REST URL을 호출해도 서버가 401/403을 반환하도록 구현해야 합니다.
685
+
686
+ ## 10. 접근성·반응형·WordPress 호환성
687
+
688
+ ### 10.1 키보드와 focus
689
+
690
+ - 모든 action을 키보드만으로 실행할 수 있어야 합니다.
691
+ - focus indicator를 제거하지 않습니다.
692
+ - modal이 열리면 focus가 modal 안으로 이동하고, 닫힌 뒤 트리거로 돌아와야 합니다.
693
+ - Escape로 modal/drawer를 닫을 수 있어야 합니다.
694
+ - sidebar toggle은 `aria-expanded`와 명확한 `aria-label`을 가집니다.
695
+ - icon-only button은 visible label 또는 `aria-label`을 가집니다.
696
+
697
+ ### 10.2 반응형 기준
698
+
699
+ 최소한 다음 viewport에서 확인합니다.
700
+
701
+ | viewport | 확인할 것 |
702
+ | --- | --- |
703
+ | 1280px | sidebar와 main content의 기본 밀도 |
704
+ | 1024px | 좁은 admin content에서 header action과 table overflow |
705
+ | 782px | WordPress 관리자 모바일 breakpoint, sidebar drawer |
706
+ | 480px | filter action wrapping, table 대체 표현, 긴 제목 |
707
+
708
+ 페이지 전체에 `overflow-x: hidden`을 적용해 문제를 감추지 않습니다. 표가 넓으면 의미 있는 horizontal scroll container나 responsive row layout을 제공합니다.
709
+
710
+ ### 10.3 기타 기준
711
+
712
+ - RTL에서 sidebar, icon, breadcrumb, back action의 방향을 확인합니다.
713
+ - `prefers-reduced-motion: reduce`에서 불필요한 transition을 줄입니다.
714
+ - WordPress color scheme을 전역으로 덮어쓰지 않습니다.
715
+ - 패키지의 dark token을 명시적으로 사용하려는 경우 root에 `data-theme="dark"`를 설정합니다.
716
+
717
+ ```html
718
+ <div class="wrap designbase-wp-admin" data-theme="dark">
719
+ <!-- 플러그인 내부 화면 -->
720
+ </div>
721
+ ```
722
+
723
+ - 현대 브라우저를 기준으로 하되, 핵심 form submit·link·native input 동작은 Web Component가 실패해도 사용할 수 있게 설계합니다.
724
+ - IE11 전용 polyfill은 추가하지 않습니다.
725
+
726
+ ## 11. CSS 격리와 커스텀 스타일
727
+
728
+ 플러그인 고유 스타일도 같은 root 아래에 둡니다.
729
+
730
+ ```scss
731
+ .designbase-wp-admin.myplugin-admin {
732
+ .myplugin-custom-card {
733
+ /* 플러그인 고유 스타일 */
734
+ }
735
+ }
736
+ ```
737
+
738
+ 다음 패턴은 금지합니다.
739
+
740
+ ```scss
741
+ /* 금지: WordPress와 다른 플러그인에 영향을 준다. */
742
+ button { ... }
743
+ table { ... }
744
+ body { ... }
745
+ #adminmenu { ... }
746
+ ```
747
+
748
+ 다음 현상이 있으면 먼저 root와 enqueue를 확인합니다.
749
+
750
+ - input/select/button이 브라우저 기본 모양으로 보임: `styles.css`가 빠졌거나 root 밖에 렌더링된 경우가 많습니다.
751
+ - 아이콘이 네모 또는 빈 공간으로 보임: font 파일 경로, CSS font-family, 배포 파일 누락을 확인합니다.
752
+ - `db-*` element가 평범한 빈 element처럼 보임: `browser.iife.js`가 로드되지 않았거나 custom element 등록 전에 markup이 실행된 경우입니다.
753
+ - WordPress 다른 화면까지 모양이 바뀜: root 밖의 전역 selector가 존재하는지 확인합니다.
754
+ - sidebar 모바일 toggle이 작동하지 않음: `data-dbwp-sidebar-toggle` 대상 ID, browser runtime, `aria-expanded`를 확인합니다.
755
+
756
+ ## 12. 기존 플러그인과 테마 적용 순서
757
+
758
+ 기존 `dewp-*` 클래스나 이전 UI 라이브러리를 자동 치환하지 않습니다. 새 기준은 `wordpress-ui`이고, 화면 단위로 점진 적용합니다.
759
+
760
+ 1. 대상 화면의 `screen_id`와 필요한 capability를 기록합니다.
761
+ 2. 화면 root에 `.designbase-wp-admin`을 추가합니다.
762
+ 3. 해당 화면에만 `styles.css`와 `browser.iife.js`를 enqueue합니다.
763
+ 4. 기존 페이지 header/content 영역을 `AdminPageHeader` 또는 `db-page-header` 구조로 바꿉니다.
764
+ 5. 반복되는 section을 `AdminSection` 또는 `db-section`으로 정리합니다.
765
+ 6. 검색·필터 화면에 `AdminFilterBar` 또는 native-first filter markup을 적용합니다.
766
+ 7. 목록 화면에 `AdminDataTable` 또는 semantic native table을 적용합니다.
767
+ 8. loading·empty·error·success 상태를 분리합니다.
768
+ 9. modal, confirm, toast, drawer를 필요한 화면에만 추가합니다.
769
+ 10. REST·AJAX·Settings API·capability 동작은 UI migration 중 변경하지 않고 회귀 테스트합니다.
770
+
771
+ 새 플러그인은 처음부터 이 구조로 시작합니다. 기존 제품은 핵심 목록 화면 하나를 pilot으로 삼고, 시각·접근성·자산 로딩을 검증한 다음 나머지 화면으로 확장합니다.
772
+
773
+ ### 12.1 테마 관리자 적용 시 주의점
774
+
775
+ 테마 설정 화면도 플러그인과 같은 `wordpress-ui` public API를 사용합니다.
776
+
777
+ - 테마 전용 admin page와 settings page만 allowlist에 포함합니다.
778
+ - 프런트엔드 방문자 화면에는 `admin_enqueue_scripts` 자산을 로드하지 않습니다.
779
+ - 부모/자식 테마 중 어느 쪽이 vendor와 build output을 소유하는지 정합니다.
780
+ - 테마 변경이나 비활성화 시 설정 데이터의 생명주기를 UI migration과 섞지 않습니다.
781
+ - WordPress 전역 Appearance 메뉴나 admin bar를 재설계하지 않습니다.
782
+
783
+ ## 13. 테스트 및 완료 기준
784
+
785
+ ### 13.1 라이브러리 쪽
786
+
787
+ ```bash
788
+ npm run typecheck
789
+ npm run build
790
+ npm run verify
791
+ npm run audit:axe
792
+ npm run build-storybook
793
+ ```
794
+
795
+ 확인 항목:
796
+
797
+ - 선택된 ui-wc 컴포넌트가 모두 등록되는가
798
+ - `styles.css`에 theme token alias가 포함되는가
799
+ - icon font와 font asset이 포함되는가
800
+ - 모든 컴포넌트 스타일이 `.designbase-wp-admin` 아래로 scope되는가
801
+ - React adapter가 optional peer dependency로 유지되는가
802
+ - vanilla 예제가 React 없이 동작하는가
803
+ - modal focus trap과 Escape가 동작하는가
804
+
805
+ ### 13.2 소비 플러그인/테마 쪽
806
+
807
+ - 허용된 admin screen에서만 자산이 로드되는가
808
+ - 실제 배포 zip에 CSS, JS, WOFF가 포함되는가
809
+ - CDN 요청이 없는가
810
+ - `#adminmenu`, `#wpadminbar`, 다른 notice에 CSS leakage가 없는가
811
+ - REST 401/403과 nonce 만료가 올바르게 표시되는가
812
+ - 권한이 낮은 사용자가 직접 endpoint를 호출해도 서버가 차단하는가
813
+ - 1280px, 1024px, 782px, 480px에서 레이아웃이 유지되는가
814
+ - 키보드 전용 사용, focus, Escape, reduced motion, RTL을 확인했는가
815
+ - PHP lint, JS/TS typecheck, ESLint, build가 통과하는가
816
+ - 업데이트 전후에 Settings API와 기존 데이터가 유지되는가
817
+
818
+ 간단한 수동 검수 순서는 다음과 같습니다.
819
+
820
+ 1. 관리자 화면의 Network에서 `styles.css`, `browser.iife.js`, font가 한 번씩만 로드되는지 확인합니다.
821
+ 2. 다른 WordPress 관리자 화면으로 이동해 스타일이 변하지 않는지 확인합니다.
822
+ 3. 낮은 capability 사용자로 페이지와 REST endpoint를 각각 확인합니다.
823
+ 4. 브라우저 zoom 200%와 키보드만으로 주요 flow를 실행합니다.
824
+ 5. 페이지 새로고침, 뒤로가기, 필터 초기화, 빈 결과, 서버 오류를 확인합니다.
825
+ 6. 최종 zip을 깨끗한 WordPress 설치에서 설치·활성화·업데이트합니다.
826
+
827
+ ## 14. 문제 해결 체크리스트
828
+
829
+ ### 컴포넌트가 못생긴 기본 HTML처럼 보이는 경우
830
+
831
+ 1. `styles.css`가 현재 screen에 enqueue되었는지 확인합니다.
832
+ 2. HTML 최상위에 `.designbase-wp-admin`이 있는지 확인합니다.
833
+ 3. 별도의 `theme.css`를 누락한 문제가 아니라, 배포된 `wordpress-ui/dist/styles.css`를 사용하고 있는지 확인합니다.
834
+ 4. custom CSS가 `button`, `input`, `select`를 덮어쓰지 않는지 확인합니다.
835
+ 5. 패키지 버전과 vendor dist가 서로 다른 버전이 아닌지 확인합니다.
836
+
837
+ ### 아이콘이 안 보이는 경우
838
+
839
+ 1. `icons.woff2`와 `icons.woff`가 함께 배포되었는지 확인합니다.
840
+ 2. CSS의 relative URL 기준이 `dist/styles.css` 위치와 일치하는지 확인합니다.
841
+ 3. 아이콘 class가 `icon-dashboard`처럼 유효한 이름인지 확인합니다.
842
+ 4. `font-family`를 WordPress Dashicons나 다른 플러그인이 덮어쓰지 않는지 확인합니다.
843
+
844
+ ### `db-*` element가 작동하지 않는 경우
845
+
846
+ 1. `browser.iife.js`가 200으로 응답하는지 확인합니다.
847
+ 2. 플러그인 script의 dependency가 `myplugin-wordpress-ui-browser`인지 확인합니다.
848
+ 3. 콘솔에 custom element 등록 오류가 없는지 확인합니다.
849
+ 4. React entry를 vanilla HTML에 enqueue하고 있지 않은지 확인합니다.
850
+ 5. 필요한 attribute/property가 해당 component 계약과 맞는지 확인합니다.
851
+
852
+ ### REST 요청이 401/403인 경우
853
+
854
+ 1. `wp_localize_script()`가 실행되었고 object 이름이 JS와 일치하는지 확인합니다.
855
+ 2. `X-WP-Nonce` header가 전송되는지 확인합니다.
856
+ 3. 현재 사용자의 capability가 route의 `permission_callback`과 일치하는지 확인합니다.
857
+ 4. nonce action이 `wp_create_nonce('wp_rest')`와 REST cookie 인증 흐름에 맞는지 확인합니다.
858
+ 5. endpoint URL에 namespace와 trailing path가 중복되지 않았는지 확인합니다.
859
+
860
+ ## 15. 팀에서 지켜야 할 API 사용 규칙
861
+
862
+ ```ts
863
+ // 허용
864
+ import '@designbasekorea/wordpress-ui/browser';
865
+ import '@designbasekorea/wordpress-ui/styles.css';
866
+
867
+ // React 화면에서만 허용
868
+ import { AdminShell, AdminDataTable } from '@designbasekorea/wordpress-ui/react';
869
+
870
+ // 금지: 소비 플러그인/테마에서 직접 import하지 않는다.
871
+ import '@designbasekorea/ui-wc';
872
+ import { DbButton } from '@designbasekorea/ui-wc/components/db-button';
873
+ ```
874
+
875
+ 직접 `ui-wc`를 사용하면 관리자용 token scope, 아이콘 경로, 등록 allowlist, CSS 격리 정책이 패키지 밖으로 새어 나갑니다. 공통 요구사항이 생기면 소비자에서 임시로 우회하지 말고 `wordpress-ui`의 공개 API로 승격할지 먼저 검토합니다.
876
+
877
+ ## 16. 공식 참고 문서
878
+
879
+ - [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/)
880
+ - [`admin_enqueue_scripts`](https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/)
881
+ - [Settings API](https://developer.wordpress.org/plugins/settings/)
882
+ - [WordPress REST API](https://developer.wordpress.org/rest-api/)
883
+ - [WordPress API Security](https://developer.wordpress.org/apis/security/)
884
+ - [Nonces](https://developer.wordpress.org/apis/security/nonces/)
885
+ - [Checking User Capabilities](https://developer.wordpress.org/apis/security/checking-user-capabilities/)
886
+ - [Sanitizing Data](https://developer.wordpress.org/apis/security/sanitizing/)
887
+ - [Escaping Data](https://developer.wordpress.org/apis/security/escaping/)
888
+ - [`@wordpress/components`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/)
889
+
890
+ `@wordpress/components`는 WordPress React runtime과 결합된 선택적 adapter로 검토할 수 있지만, vanilla/PHP 플러그인과 테마의 공통 기반으로 추가하지 않습니다. 이 가이드의 기본 기반은 `wordpress-ui`의 React-free browser runtime입니다.