@xfilecom/xframe 0.1.34 → 0.1.36

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.
Files changed (40) hide show
  1. package/bin/xframe.js +12 -10
  2. package/defaults.json +2 -2
  3. package/package.json +1 -1
  4. package/template/README.md +1 -1
  5. package/template/apps/api/src/main.ts +53 -13
  6. package/template/shared/endpoint/README.md +30 -1
  7. package/template/shared/endpoint/endpoint.ts +55 -0
  8. package/template/shared/endpoint/index.ts +2 -0
  9. package/template/web/admin/src/App.tsx +2 -7
  10. package/template/web/admin/src/main.tsx +12 -1
  11. package/template/web/admin/tsconfig.json +1 -1
  12. package/template/web/admin/vite.config.ts +2 -7
  13. package/template/web/client/src/App.tsx +2 -7
  14. package/template/web/client/src/main.tsx +12 -1
  15. package/template/web/client/tsconfig.json +1 -1
  16. package/template/web/client/vite.config.ts +4 -7
  17. package/template/web/shared/README.md +4 -5
  18. package/template/web/shared/package.json +3 -1
  19. package/template/web/shared/src/api/client.ts +132 -0
  20. package/template/web/shared/src/api/methods/health.method.ts +14 -0
  21. package/template/web/shared/src/api/methods/index.ts +31 -0
  22. package/template/web/shared/src/api/request.ts +65 -0
  23. package/template/web/shared/src/api/types.ts +80 -0
  24. package/template/web/shared/src/context/StoreProvider.tsx +45 -0
  25. package/template/web/shared/src/hooks/useAppStore.ts +23 -0
  26. package/template/web/shared/src/hooks/useHealthStatus.ts +12 -3
  27. package/template/web/shared/src/index.ts +53 -1
  28. package/template/web/shared/src/lib/http.ts +9 -6
  29. package/template/web/shared/src/methods/health.ts +5 -4
  30. package/template/web/shared/src/params/param-store.ts +178 -0
  31. package/template/web/shared/src/params/types.ts +21 -0
  32. package/template/web/shared/src/params/validations.ts +32 -0
  33. package/template/web/shared/src/store/api-cache-store.ts +35 -0
  34. package/template/web/shared/src/store/index.ts +4 -0
  35. package/template/web/shared/src/store/root-store.ts +358 -0
  36. package/template/web/shared/src/store/session-store.ts +28 -0
  37. package/template/web/shared/src/types/ui.ts +37 -0
  38. package/template/web/shared/tsconfig.json +4 -1
  39. package/template/yarn.lock +237 -13
  40. package/template/web/shared/src/stores/appStore.ts +0 -11
package/bin/xframe.js CHANGED
@@ -442,10 +442,9 @@ function executeDbPull(cwd) {
442
442
  return false;
443
443
  }
444
444
 
445
- /** 모노레포 템플릿 tsconfig 의 @xfilecom/front-core 경로 → 생성 앱의 node_modules 기준 */
446
- const TEMPLATE_FRONT_CORE_TS_PATH = '../../../../front-core/src/index.ts';
447
- const SCAFFOLD_FRONT_CORE_TS_PATH = '../../node_modules/@xfilecom/front-core';
448
-
445
+ /**
446
+ * 스캐폴드 tsconfig 의 paths.@xfilecom/front-core 제거 → 설치된 패키지(node_modules) 기준 해석.
447
+ */
449
448
  function patchWebTsconfigFrontCorePath(targetRoot) {
450
449
  for (const sub of ['client', 'admin']) {
451
450
  const p = path.join(targetRoot, 'web', sub, 'tsconfig.json');
@@ -456,17 +455,20 @@ function patchWebTsconfigFrontCorePath(targetRoot) {
456
455
  } catch {
457
456
  continue;
458
457
  }
459
- const arr = j?.compilerOptions?.paths?.['@xfilecom/front-core'];
460
- if (Array.isArray(arr) && arr[0] === TEMPLATE_FRONT_CORE_TS_PATH) {
461
- j.compilerOptions.paths['@xfilecom/front-core'] = [SCAFFOLD_FRONT_CORE_TS_PATH];
462
- fs.writeFileSync(p, `${JSON.stringify(j, null, 2)}\n`, 'utf8');
458
+ const paths = j?.compilerOptions?.paths;
459
+ if (!paths || !Object.prototype.hasOwnProperty.call(paths, '@xfilecom/front-core')) {
460
+ continue;
461
+ }
462
+ delete paths['@xfilecom/front-core'];
463
+ if (Object.keys(paths).length === 0) {
464
+ delete j.compilerOptions.paths;
463
465
  }
466
+ fs.writeFileSync(p, `${JSON.stringify(j, null, 2)}\n`, 'utf8');
464
467
  }
465
468
  }
466
469
 
467
470
  /**
468
- * Vite 에서 @xfilecom/front-core node_modules 패키지 "루트"만 alias 하면
469
- * `tokens.css` 같은 서브패스가 exports 를 타지 못해 깨짐 → alias 제거 후 npm 기본 해석.
471
+ * 구 템플릿 Vite @xfilecom/front-core 소스 디렉터리 alias 제거(npm 패키지·exports 사용).
470
472
  */
471
473
  function patchWebViteFrontCoreAlias(targetRoot) {
472
474
  for (const sub of ['client', 'admin']) {
package/defaults.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "backendCore": "^1.0.14",
3
- "frontCore": "^0.2.20"
2
+ "backendCore": "^1.0.16",
3
+ "frontCore": "^0.2.22"
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfilecom/xframe",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Scaffold full-stack app: Nest + @xfilecom/backend-core, Vite/React + @xfilecom/front-core",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -25,7 +25,7 @@ Nest API (`@xfilecom/backend-core`) + Vite/React client·admin (`@xfilecom/front
25
25
  ## `shared/` (설정·스키마·SQL·endpoint)
26
26
 
27
27
  - **`shared/config/api`**, **`shared/config/web/client`**, **`shared/config/web/admin`**: YAML (`application.yml` + `application-<env>.yml`)
28
- - **`web/shared`**: `components`, `hooks`, `stores` (zustand), `methods`, `lib`, `types`, `constants`, `utils`
28
+ - **`web/shared`**: `store/` (RootStore·session·cache), `params/` (ParamStore), `api/` (client·request·methods), `context/` (StoreProvider), `components`, `hooks`, `methods`, `lib`, `types`, `constants`, `utils`
29
29
  - **`shared/schema`**, **`shared/sql`**, **`shared/endpoint`**: 팀이 채우는 자산(README 참고)
30
30
 
31
31
  자세한 설명은 `shared/README.md` 참고.
@@ -5,7 +5,21 @@ import type { INestApplication } from '@nestjs/common';
5
5
  import { AppModule } from './app.module';
6
6
  import { appConfig } from './config.loader';
7
7
 
8
- function applyCors(app: INestApplication): void {
8
+ function parsePositivePort(v: string | number | undefined): number | undefined {
9
+ const n = typeof v === 'number' ? v : Number(v);
10
+ return Number.isFinite(n) && n > 0 ? n : undefined;
11
+ }
12
+
13
+ /** `PORT`(syncEnvKeys) → `server.port`(YAML) → 기본값 */
14
+ function resolveListenPort(): number {
15
+ return (
16
+ parsePositivePort(process.env.PORT) ??
17
+ parsePositivePort(appConfig.server?.port) ??
18
+ 3000
19
+ );
20
+ }
21
+
22
+ function applyCorsFromConfig(app: INestApplication): void {
9
23
  const cors = appConfig.cors;
10
24
  if (cors?.enabled === false) {
11
25
  return;
@@ -19,26 +33,52 @@ function applyCors(app: INestApplication): void {
19
33
  app.enableCors({ origin: origin === true, credentials });
20
34
  }
21
35
 
22
- function applyHttp(app: INestApplication): void {
36
+ function applyHttpFromConfig(app: INestApplication): void {
23
37
  const prefix = appConfig.http?.globalPrefix;
24
38
  if (prefix != null && String(prefix).trim() !== '') {
25
39
  app.setGlobalPrefix(String(prefix).replace(/^\/+|\/+$/g, ''));
26
40
  }
27
41
  }
28
42
 
43
+ /**
44
+ * Nest 인스턴스에 직접 걸 수 있는 설정만 반영합니다.
45
+ * - DB 연결·JWT·인터셉터 등: `config.loader`의 syncEnvKeys + `AppModule`(CoreModule)
46
+ */
47
+ function applyNestAppFromConfig(app: INestApplication): void {
48
+ applyCorsFromConfig(app);
49
+ applyHttpFromConfig(app);
50
+ }
51
+
52
+ function logStartupFromConfig(port: number): void {
53
+ const base = appConfig.http?.publicBaseUrl?.replace(/\/$/, '');
54
+ const pfx = appConfig.http?.globalPrefix
55
+ ? `/${String(appConfig.http.globalPrefix).replace(/^\/+|\/+$/g, '')}`
56
+ : '';
57
+ const env =
58
+ process.env.APP_ENV ??
59
+ appConfig.app?.env ??
60
+ process.env.NODE_ENV ??
61
+ 'development';
62
+ const dbOn = appConfig.core?.database?.auto === true;
63
+ const dbHost = appConfig.database?.host;
64
+ const dbName = appConfig.database?.name;
65
+ const jwtGuard = appConfig.core?.guards?.jwt === true;
66
+ const sqlEp = appConfig.core?.sqlEndpoint?.enabled === true;
67
+ // eslint-disable-next-line no-console
68
+ console.log(
69
+ `[api] listen :${port} env=${env}` +
70
+ (base ? ` public=${base}${pfx}` : '') +
71
+ ` cors=${appConfig.cors?.enabled !== false ? 'on' : 'off'}` +
72
+ ` db=${dbOn ? 'auto' : 'off'}${dbHost ? `@${dbHost}` : ''}${dbName ? `/${dbName}` : ''}` +
73
+ ` jwtGuard=${jwtGuard} sqlEndpoint=${sqlEp}`,
74
+ );
75
+ }
76
+
29
77
  async function bootstrap() {
30
78
  const app = await NestFactory.create(AppModule);
31
- applyCors(app);
32
- applyHttp(app);
33
- const port = Number(process.env.PORT) || 3000;
34
- const base = appConfig.http?.publicBaseUrl?.replace(/\/$/, '');
79
+ applyNestAppFromConfig(app);
80
+ const port = resolveListenPort();
35
81
  await app.listen(port);
36
- if (base) {
37
- const pfx = appConfig.http?.globalPrefix
38
- ? `/${String(appConfig.http.globalPrefix).replace(/^\/+|\/+$/g, '')}`
39
- : '';
40
- // eslint-disable-next-line no-console
41
- console.log(`Listening ${base}${pfx} (port ${port})`);
42
- }
82
+ logStartupFromConfig(port);
43
83
  }
44
84
  bootstrap();
@@ -1,4 +1,33 @@
1
1
  # Endpoint / API 계약
2
2
 
3
- - OpenAPI `openapi.yaml`, 공유 요청·응답 타입, 라우트 prefix 규칙 등을 둡니다.
3
+ - **`endpoint.ts`** `EndpointDef`, `HttpMethod`, 엔드포인트 상수(예: `healthEndpoint`). 프론트 `sendRequest`·백엔드 라우트와 동일 경로를 맞출 때 사용합니다.
4
+
5
+ ### `EndpointDef` 필드 요약
6
+
7
+ | 필드 | Nest | 프론트 |
8
+ |------|------|--------|
9
+ | `method`, `path` | 라우트와 맞추기 | `sendRequest` URL·HTTP 메서드 |
10
+ | `post` | 무시 | **응답 본문**(`axios`의 `response.data`와 동일 한 덩어리)만 인자로 받아 가공 후 반환 |
11
+ | `showIndicator` | 무시 | `sendMessage` 시 옵션 미지정이면 전역 로딩 기본값 |
12
+ | `timeoutMs` | 무시 | 해당 요청만 axios `timeout` |
13
+
14
+ **응답 가공**은 엔드포인트 단위 `post` 와, `web/shared` 의 `ApiMethodConfig`·`RootStore.execute` 의 `unwrapDataArray` / `post(result, stores)` 를 함께 쓸 수 있습니다. 같은 데이터를 두 번 벗기지 않도록 한쪽만 쓰는 것을 권장합니다.
15
+
16
+ `post` 예시 — 인자는 **HTTP 본문 JSON**이지, axios `Response` 객체가 아닙니다.
17
+
18
+ ```ts
19
+ import { unwrapResponseData, type EndpointDef } from '@shared/endpoint';
20
+
21
+ export const itemEndpoint: EndpointDef = {
22
+ method: 'GET',
23
+ path: '/items/:id',
24
+ post: unwrapResponseData, // 또는 (body) => (body as { data: Item }).data
25
+ showIndicator: true,
26
+ timeoutMs: 10_000,
27
+ };
28
+ ```
29
+
30
+ - OpenAPI `openapi.yaml`, 공유 요청·응답 타입, 라우트 prefix 규칙 등도 이 디렉터리·형제 파일에 둘 수 있습니다.
4
31
  - 실제 Nest `Controller`는 `apps/api/src`에 두고, 여기서는 **계약·문서**를 단일 소스로 유지하는 패턴을 권장합니다.
32
+
33
+ 임포트: `import { healthEndpoint, type EndpointDef } from '@shared/endpoint';`
@@ -0,0 +1,55 @@
1
+ /**
2
+ * HTTP API 엔드포인트 정의 — Nest(`@shared/*`)와 Vite(`@shared/*` alias) 공통.
3
+ * `web/` 옆 루트 `shared/endpoint/` 에 두어 백엔드 라우트·프론트 axios 경로를 한곳에서 맞춥니다.
4
+ *
5
+ * Nest 는 주로 `method`·`path` 만 쓰고, `post`·`showIndicator`·`timeoutMs` 등은 프론트 `sendRequest` / RootStore `sendMessage` 가 참고합니다.
6
+ */
7
+
8
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
+
10
+ /**
11
+ * 백엔드 `CommonResponseDto` 류 `{ data: T, code?, error? }` 에서 `data` 만 꺼냄.
12
+ * 해당 필드가 없으면 본문 그대로 반환.
13
+ */
14
+ export function unwrapResponseData(body: unknown): unknown {
15
+ if (body !== null && typeof body === 'object' && 'data' in body) {
16
+ return (body as { data: unknown }).data;
17
+ }
18
+ return body;
19
+ }
20
+
21
+ export interface EndpointDef {
22
+ method: HttpMethod;
23
+ /** 상대 경로. `:id` 는 sendRequest 의 params 로 치환 */
24
+ path: string;
25
+
26
+ /**
27
+ * axios 가 파싱한 **응답 본문**(`response.data`)만 넘어옵니다. AxiosResponse 전체가 아닙니다.
28
+ * `{ data: T }` 언래핑은 `unwrapResponseData` 또는 `(body) => (body as { data: T }).data` 패턴을 쓰세요.
29
+ */
30
+ post?: (body: unknown) => unknown;
31
+
32
+ /**
33
+ * RootStore.sendMessage 에서 `options.showIndicator` 가 없을 때 기본값.
34
+ * `sendRequest` 직호출 시에는 axios 훅만 쓰므로 이 필드는 무시됩니다.
35
+ */
36
+ showIndicator?: boolean;
37
+
38
+ /** 이 요청만 다른 타임아웃(ms). 미설정 시 apiClient 기본값 */
39
+ timeoutMs?: number;
40
+ }
41
+
42
+ export const healthEndpoint: EndpointDef = {
43
+ method: 'GET',
44
+ path: '/health',
45
+ /** health 는 인디케이터 없이 조용히 호출하는 경우가 많음 */
46
+ showIndicator: false,
47
+ };
48
+
49
+ export const appMetaEndpoint: EndpointDef = {
50
+ method: 'GET',
51
+ path: '/app-meta',
52
+ post: unwrapResponseData,
53
+ showIndicator: false,
54
+ timeoutMs: 10_000,
55
+ };
@@ -0,0 +1,2 @@
1
+ export type { HttpMethod, EndpointDef } from './endpoint';
2
+ export { healthEndpoint, appMetaEndpoint, unwrapResponseData } from './endpoint';
@@ -1,15 +1,10 @@
1
1
  import { Badge, Text } from '@xfilecom/front-core';
2
- import {
3
- Shell,
4
- useHealthStatus,
5
- FALLBACK_API_BASE_URL,
6
- } from '__WEB_SHARED_WORKSPACE__';
2
+ import { Shell, useHealthStatus } from '__WEB_SHARED_WORKSPACE__';
7
3
 
8
- const apiBase = import.meta.env.VITE_API_BASE_URL || FALLBACK_API_BASE_URL;
9
4
  const title = import.meta.env.VITE_APP_TITLE || '__PACKAGE_NAME__ (admin)';
10
5
 
11
6
  export function App() {
12
- const { text, error } = useHealthStatus(apiBase);
7
+ const { text, error } = useHealthStatus();
13
8
 
14
9
  return (
15
10
  <Shell
@@ -4,10 +4,21 @@ import '@xfilecom/front-core/tokens.css';
4
4
  import '@xfilecom/front-core/base.css';
5
5
  import '__WEB_SHARED_WORKSPACE__/styles/xfc-theme.css';
6
6
  import '__WEB_SHARED_WORKSPACE__/styles/app.css';
7
+ import {
8
+ StoreProvider,
9
+ configureApi,
10
+ FALLBACK_API_BASE_URL,
11
+ } from '__WEB_SHARED_WORKSPACE__';
7
12
  import { App } from './App';
8
13
 
14
+ configureApi({
15
+ baseURL: import.meta.env.VITE_API_BASE_URL || FALLBACK_API_BASE_URL,
16
+ });
17
+
9
18
  ReactDOM.createRoot(document.getElementById('root')!).render(
10
19
  <React.StrictMode>
11
- <App />
20
+ <StoreProvider>
21
+ <App />
22
+ </StoreProvider>
12
23
  </React.StrictMode>,
13
24
  );
@@ -10,7 +10,7 @@
10
10
  "paths": {
11
11
  "__WEB_SHARED_WORKSPACE__": ["../shared/src/index.ts"],
12
12
  "__WEB_SHARED_WORKSPACE__/*": ["../shared/src/*"],
13
- "@xfilecom/front-core": ["../../../../front-core/src/index.ts"]
13
+ "@shared/*": ["../../shared/*"]
14
14
  },
15
15
  "allowImportingTsExtensions": true,
16
16
  "resolveJsonModule": true,
@@ -7,13 +7,8 @@ import react from '@vitejs/plugin-react';
7
7
 
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
 
10
- /**
11
- * xframe 레포 안 템플릿: …/packages/xframe/packages/front-core/src
12
- * (`npx @xfilecom/xframe <dir>` 생성 시 CLI 가 front-core alias·const 를 제거 → npm exports 로 해석)
13
- */
14
- const frontCoreSrc = path.resolve(__dirname, '../../../..', 'front-core', 'src');
15
-
16
10
  const webSharedSrc = path.resolve(__dirname, '../shared/src');
11
+ const rootSharedSrc = path.resolve(__dirname, '../../shared');
17
12
 
18
13
  function readYaml(file: string): Record<string, unknown> {
19
14
  if (!existsSync(file)) return {};
@@ -69,7 +64,7 @@ export default defineConfig(({ mode }) => {
69
64
  plugins: [react()],
70
65
  resolve: {
71
66
  alias: {
72
- '@xfilecom/front-core': frontCoreSrc,
67
+ '@shared': rootSharedSrc,
73
68
  '__WEB_SHARED_WORKSPACE__': webSharedSrc,
74
69
  },
75
70
  },
@@ -1,20 +1,15 @@
1
1
  import { useState } from 'react';
2
2
  import { Badge, Button, Stack, Text } from '@xfilecom/front-core';
3
- import {
4
- Shell,
5
- useHealthStatus,
6
- FALLBACK_API_BASE_URL,
7
- } from '__WEB_SHARED_WORKSPACE__';
3
+ import { Shell, useHealthStatus } from '__WEB_SHARED_WORKSPACE__';
8
4
  import { FrontCoreShowcase } from './FrontCoreShowcase';
9
5
 
10
- const apiBase = import.meta.env.VITE_API_BASE_URL || FALLBACK_API_BASE_URL;
11
6
  const title = import.meta.env.VITE_APP_TITLE || '__PACKAGE_NAME__';
12
7
 
13
8
  type Tab = 'api' | 'atoms';
14
9
 
15
10
  export function App() {
16
11
  const [tab, setTab] = useState<Tab>('atoms');
17
- const { text, error } = useHealthStatus(apiBase);
12
+ const { text, error } = useHealthStatus();
18
13
 
19
14
  return (
20
15
  <Shell
@@ -4,10 +4,21 @@ import '@xfilecom/front-core/tokens.css';
4
4
  import '@xfilecom/front-core/base.css';
5
5
  import '__WEB_SHARED_WORKSPACE__/styles/xfc-theme.css';
6
6
  import '__WEB_SHARED_WORKSPACE__/styles/app.css';
7
+ import {
8
+ StoreProvider,
9
+ configureApi,
10
+ FALLBACK_API_BASE_URL,
11
+ } from '__WEB_SHARED_WORKSPACE__';
7
12
  import { App } from './App';
8
13
 
14
+ configureApi({
15
+ baseURL: import.meta.env.VITE_API_BASE_URL || FALLBACK_API_BASE_URL,
16
+ });
17
+
9
18
  ReactDOM.createRoot(document.getElementById('root')!).render(
10
19
  <React.StrictMode>
11
- <App />
20
+ <StoreProvider>
21
+ <App />
22
+ </StoreProvider>
12
23
  </React.StrictMode>,
13
24
  );
@@ -10,7 +10,7 @@
10
10
  "paths": {
11
11
  "__WEB_SHARED_WORKSPACE__": ["../shared/src/index.ts"],
12
12
  "__WEB_SHARED_WORKSPACE__/*": ["../shared/src/*"],
13
- "@xfilecom/front-core": ["../../../../front-core/src/index.ts"]
13
+ "@shared/*": ["../../shared/*"]
14
14
  },
15
15
  "allowImportingTsExtensions": true,
16
16
  "resolveJsonModule": true,
@@ -7,15 +7,12 @@ import react from '@vitejs/plugin-react';
7
7
 
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
 
10
- /**
11
- * xframe 레포 안 템플릿: …/packages/xframe/packages/front-core/src
12
- * (`npx @xfilecom/xframe <dir>` 생성 시 CLI 가 front-core alias·const 를 제거 → npm exports 로 해석)
13
- */
14
- const frontCoreSrc = path.resolve(__dirname, '../../../..', 'front-core', 'src');
15
-
16
10
  /** file:../shared 복사본이 아닌 워크스페이스 web/shared/src (플레이스홀더 패키지 이름과 동일 키) */
17
11
  const webSharedSrc = path.resolve(__dirname, '../shared/src');
18
12
 
13
+ /** 루트 `shared/` — `web/` 과 동일 레벨 (엔드포인트·스키마 등) */
14
+ const rootSharedSrc = path.resolve(__dirname, '../../shared');
15
+
19
16
  function readYaml(file: string): Record<string, unknown> {
20
17
  if (!existsSync(file)) return {};
21
18
  const parsed = yaml.load(readFileSync(file, 'utf8'));
@@ -70,7 +67,7 @@ export default defineConfig(({ mode }) => {
70
67
  plugins: [react()],
71
68
  resolve: {
72
69
  alias: {
73
- '@xfilecom/front-core': frontCoreSrc,
70
+ '@shared': rootSharedSrc,
74
71
  '__WEB_SHARED_WORKSPACE__': webSharedSrc,
75
72
  },
76
73
  },
@@ -24,16 +24,15 @@ import { Shell } from '__WEB_SHARED_WORKSPACE__';
24
24
 
25
25
  새 파일을 앱에서 가져오려면 `package.json`의 **`exports`**에 서브패스를 추가하세요.
26
26
 
27
- ## `@xfilecom/front-core`를 모노레포에서 고치는 경우
27
+ ## `@xfilecom/front-core`
28
28
 
29
- `web/client`·`web/admin`의 **Vite `resolve.alias`**가 형제 디렉터리의 `front-core/src`(xframe 모노레포 기준 `packages/front-core/src`)를 가리키도록 되어 있으면, npm `dist/`가 아니라 **로컬 소스**가 번들됩니다. dev 중 HMR로 반영되는 것이 정상입니다.
29
+ `web/client`·`web/admin`·`web/shared`는 **`package.json` 의존성 + `node_modules`** 해석합니다(`tsconfig`·Vite에 `paths`/alias로 소스 경로를 강제하지 않음). 서브패스(`tokens.css` 등)는 패키지 `exports`를 따릅니다.
30
30
 
31
- 스캐폴드만 복사한 프로젝트처럼 경로가 없으면 alias가 깨지므로, `package.json`의 `@xfilecom/front-core` 버전과 `npm run build`(해당 패키지)로 갱신하세요.
31
+ 로컬에서 코어 소스를 쓰려면 **`npm link @xfilecom/front-core`** 또는 `package.json`의 `file:../front-core` / `workspace:` 등으로 **설치 위치**를 바꾸는 방식을 권장합니다.
32
32
 
33
33
  ### `web/shared/tsconfig.json`에는 `paths`로 front-core를 붙이지 마세요
34
34
 
35
- `rootDir`가 `src`인데 `@xfilecom/front-core`를 `../../../.../front-core/src`로 매핑하면, TypeScript가 **rootDir 소스**를 끌어와 `File is not under 'rootDir'` 오류가 납니다.
36
- `web/shared` 패키지 타입체크는 **`node_modules`의 `@xfilecom/front-core`**에 맡기고, 로컬 소스 번들은 위 **Vite alias**(client/admin)만 사용하면 됩니다.
35
+ `rootDir` 밖 소스를 끌어오면 `tsc`·에디터가 깨지기 쉽습니다. 타입은 **`node_modules/@xfilecom/front-core`**에 맡깁니다.
37
36
 
38
37
  ### `web/shared` CSS·TS가 수정해도 반영되지 않을 때
39
38
 
@@ -19,7 +19,9 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@xfilecom/front-core": "__FRONT_CORE_SPEC__",
22
- "zustand": "^5.0.3"
22
+ "axios": "^1.7.9",
23
+ "mobx": "^6.13.5",
24
+ "mobx-react-lite": "^4.0.7"
23
25
  },
24
26
  "peerDependencies": {
25
27
  "react": "^18.0.0",
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Axios — baseURL, 로깅 인터셉터, 전역 훅(onRequestStart/End), Bearer
3
+ */
4
+
5
+ import axios, {
6
+ type AxiosError,
7
+ type AxiosResponse,
8
+ type InternalAxiosRequestConfig,
9
+ } from 'axios';
10
+ import { sessionStore } from '../store/session-store';
11
+
12
+ const LOG_PREFIX = '[xframe/http]';
13
+
14
+ export type OnRequestStart = () => void;
15
+ export type OnRequestEnd = () => void;
16
+ export type OnError = (message: string, code?: string | number) => void;
17
+
18
+ let onRequestStart: OnRequestStart = () => {};
19
+ let onRequestEnd: OnRequestEnd = () => {};
20
+ let onError: OnError = () => {};
21
+
22
+ export function setApiHooks(hooks: {
23
+ onRequestStart?: OnRequestStart;
24
+ onRequestEnd?: OnRequestEnd;
25
+ onError?: OnError;
26
+ }) {
27
+ if (hooks.onRequestStart) onRequestStart = hooks.onRequestStart;
28
+ if (hooks.onRequestEnd) onRequestEnd = hooks.onRequestEnd;
29
+ if (hooks.onError) onError = hooks.onError;
30
+ }
31
+
32
+ export const apiClient = axios.create({
33
+ timeout: 30_000,
34
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
35
+ });
36
+
37
+ export function configureApi(options: { baseURL: string }) {
38
+ apiClient.defaults.baseURL = options.baseURL.replace(/\/$/, '');
39
+ }
40
+
41
+ export function getApiBaseUrl(): string {
42
+ return apiClient.defaults.baseURL ?? '';
43
+ }
44
+
45
+ type ExtConfig = InternalAxiosRequestConfig & { skipGlobalIndicator?: boolean };
46
+
47
+ function logRequest(config: InternalAxiosRequestConfig) {
48
+ console.log(`${LOG_PREFIX} request`, {
49
+ method: config.method?.toUpperCase(),
50
+ url: config.url,
51
+ baseURL: config.baseURL,
52
+ params: config.params,
53
+ data: config.data,
54
+ headers: config.headers,
55
+ });
56
+ }
57
+
58
+ function logResponseSuccess(response: AxiosResponse) {
59
+ console.log(`${LOG_PREFIX} response`, {
60
+ status: response.status,
61
+ statusText: response.statusText,
62
+ url: response.config.url,
63
+ data: response.data,
64
+ });
65
+ }
66
+
67
+ function logResponseError(error: unknown) {
68
+ if (axios.isAxiosError(error)) {
69
+ console.log(`${LOG_PREFIX} response error`, {
70
+ message: error.message,
71
+ code: error.code,
72
+ status: error.response?.status,
73
+ statusText: error.response?.statusText,
74
+ url: error.config?.url,
75
+ data: error.response?.data,
76
+ });
77
+ } else {
78
+ console.log(`${LOG_PREFIX} response error`, error);
79
+ }
80
+ }
81
+
82
+ apiClient.interceptors.request.use(
83
+ (config: InternalAxiosRequestConfig) => {
84
+ logRequest(config);
85
+ const c = config as ExtConfig;
86
+ const token = sessionStore.token;
87
+ if (token && c.headers) {
88
+ c.headers.Authorization = `Bearer ${token}`;
89
+ }
90
+ if (c.data instanceof FormData && c.headers) {
91
+ delete c.headers['Content-Type'];
92
+ }
93
+ if (!c.skipGlobalIndicator) onRequestStart();
94
+ return c;
95
+ },
96
+ (err) => {
97
+ console.log(`${LOG_PREFIX} request error`, err);
98
+ return Promise.reject(err);
99
+ },
100
+ );
101
+
102
+ apiClient.interceptors.response.use(
103
+ (response: AxiosResponse) => {
104
+ const c = response.config as ExtConfig;
105
+ if (!c.skipGlobalIndicator) onRequestEnd();
106
+ logResponseSuccess(response);
107
+ return response;
108
+ },
109
+ (err: AxiosError) => {
110
+ const c = err.config as ExtConfig | undefined;
111
+ if (!c?.skipGlobalIndicator) onRequestEnd();
112
+ logResponseError(err);
113
+ const res = err?.response as
114
+ | { data?: { message?: unknown; error?: unknown; code?: string | number }; status?: number }
115
+ | undefined;
116
+ const raw = res?.data?.message ?? res?.data?.error ?? (err as Error)?.message ?? 'Request failed';
117
+ const message =
118
+ typeof raw === 'string'
119
+ ? raw
120
+ : Array.isArray(raw)
121
+ ? raw.map((x) => (typeof x === 'string' ? x : String(x))).join(', ')
122
+ : typeof raw === 'object' &&
123
+ raw != null &&
124
+ 'message' in raw &&
125
+ typeof (raw as { message: unknown }).message === 'string'
126
+ ? (raw as { message: string }).message
127
+ : JSON.stringify(raw);
128
+ const code = res?.status ?? (res?.data?.code as string | number | undefined);
129
+ onError(message, code);
130
+ return Promise.reject(err);
131
+ },
132
+ );
@@ -0,0 +1,14 @@
1
+ import type { CommonEnvelope, HealthData } from '../../types/api';
2
+ import type { ApiMethodConfig, ApiMethodStore } from '../types';
3
+ import { healthEndpoint } from '@shared/endpoint';
4
+
5
+ /** GET /health — params·body 없음, 스토어는 validate/post 에서 사용 가능 */
6
+ export const healthMethod: ApiMethodConfig<void, CommonEnvelope<HealthData>, ApiMethodStore> = {
7
+ schema: {},
8
+ endpoint: healthEndpoint,
9
+ ui: {
10
+ showIndicator: false,
11
+ showErrorToast: true,
12
+ unwrapDataArray: false,
13
+ },
14
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 화면별 workflow registry — 새 method 는 *.method.ts 추가 후 여기 등록
3
+ */
4
+
5
+ import type { ParamsSchema } from '../../params/types';
6
+ import { healthMethod } from './health.method';
7
+
8
+ export type {
9
+ ApiMethodConfig,
10
+ ApiMethodStore,
11
+ ApiMethodEndpointCall,
12
+ CoreSdkWrapped,
13
+ ExecuteContext,
14
+ ApiMethodUiConfig,
15
+ ScreenMeta,
16
+ ScreenAuthType,
17
+ } from '../types';
18
+
19
+ export const methods = {
20
+ health: healthMethod,
21
+ } as const;
22
+
23
+ /** ParamStore 초기 스키마 — 화면명 → 필드 → value + valid 규칙 */
24
+ export const screenParamSchemas: ParamsSchema = {
25
+ /** 예시: 검색어 (선택) — execute 전 validate 에서 isScreenValid 로 검사 가능 */
26
+ demo: {
27
+ q: { value: '', valid: [] },
28
+ },
29
+ };
30
+
31
+ export type MethodName = keyof typeof methods;