@yeongseoksong/framework 1.6.2 → 1.7.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 +63 -1
- package/dist/platform/index.d.mts +40 -0
- package/dist/platform/index.mjs +30 -0
- package/dist/ui/index.mjs +104 -102
- package/dist/util/index.d.mts +50 -1
- package/dist/util/index.mjs +68 -0
- package/package.json +14 -33
- package/dist/store/index.cjs +0 -393
- package/dist/store/index.d.ts +0 -237
- package/dist/types/index.cjs +0 -18
- package/dist/types/index.d.ts +0 -105
- package/dist/ui/index.cjs +0 -3136
- package/dist/ui/index.d.ts +0 -1347
- package/dist/util/index.cjs +0 -199
- package/dist/util/index.d.ts +0 -114
package/README.md
CHANGED
|
@@ -144,7 +144,8 @@ import { appTheme } from './theme'
|
|
|
144
144
|
| -------------------------------- | --------------------------------------------- |
|
|
145
145
|
| `@yeongseoksong/framework/ui` | UI 컴포넌트 전체 + `theme` (`"use client"`) |
|
|
146
146
|
| `@yeongseoksong/framework/store` | Zustand 스토어 — `useAuthStore` · `useUiStore` · `useNavStore` · `useSdForm` (`"use client"`) |
|
|
147
|
-
| `@yeongseoksong/framework/util` | `t()`, 한글 조사(`josa` · `withJosa` · `fixJosa`), `runFinalizers`, `filterAndSort`, `COMPANY_NAME`, `LOGO_SRC`, `LOGO_ALT` |
|
|
147
|
+
| `@yeongseoksong/framework/util` | `t()`, 한글 조사(`josa` · `withJosa` · `fixJosa`), `runFinalizers`, `filterAndSort`, `apiClient`, `COMPANY_NAME`, `LOGO_SRC`, `LOGO_ALT` |
|
|
148
|
+
| `@yeongseoksong/framework/platform` | `apiClient`를 `useAuthStore`와 엮은 `Pl*` API — `PlAxios` 등 (`"use client"`) |
|
|
148
149
|
| `@yeongseoksong/framework/types` | 공유 인터페이스 |
|
|
149
150
|
|
|
150
151
|
---
|
|
@@ -862,6 +863,67 @@ fixJosa('서울(으)로 이전했습니다') // → '서울로 이전했습니
|
|
|
862
863
|
|
|
863
864
|
---
|
|
864
865
|
|
|
866
|
+
## API 클라이언트
|
|
867
|
+
|
|
868
|
+
axios 인스턴스는 `NEXT_PUBLIC_API_BASE_URL`을 baseURL로 쓰고, `withCredentials: true`로 **httpOnly 쿠키 인증**을 전제합니다(`useAuthStore`가 토큰을 담지 않는 이유와 같습니다 — localStorage에 토큰을 두면 XSS 한 번에 털립니다).
|
|
869
|
+
|
|
870
|
+
경로가 두 개인데, **인스턴스는 하나**입니다. `/platform`은 `/util`의 같은 객체에 `Pl` 이름과 스토어 연동을 얹은 얇은 층입니다.
|
|
871
|
+
|
|
872
|
+
| 경로 | 쓰는 경우 |
|
|
873
|
+
| ----------------------------------- | ------------------------------------------------------------ |
|
|
874
|
+
| `@yeongseoksong/framework/util` | 스토어를 쓰지 않거나 인증 처리를 직접 하겠다 — 서버 코드에서도 됩니다 |
|
|
875
|
+
| `@yeongseoksong/framework/platform` | `useAuthStore`를 쓴다 — 401이면 자동 로그아웃 (`"use client"`) |
|
|
876
|
+
|
|
877
|
+
### `/platform` — 스토어 연동
|
|
878
|
+
|
|
879
|
+
```ts
|
|
880
|
+
import { PlAxios, PlApiError, PlSetUnauthorizedHandler } from '@yeongseoksong/framework/platform'
|
|
881
|
+
|
|
882
|
+
// 401이 최종 확정되면 세션은 이미 정리된 뒤입니다 — 여기엔 앱 고유의 후처리만 씁니다.
|
|
883
|
+
PlSetUnauthorizedHandler(() => router.replace('/login'))
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
const { data } = await PlAxios.get('/me')
|
|
887
|
+
} catch (e) {
|
|
888
|
+
if (e instanceof PlApiError) console.error(e.status, e.code, e.message)
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
이 모듈을 import하는 것만으로 401 인터셉터가 `useAuthStore.getState().logout()`을 부르도록 등록됩니다. 슬롯이 하나뿐이므로 **`setUnauthorizedHandler`를 직접 부르지 마세요** — 부르면 로그아웃이 통째로 밀려납니다. `PlSetUnauthorizedHandler`가 그 뒤에 이어 붙는 자리입니다.
|
|
893
|
+
|
|
894
|
+
`PlApiError`는 `/util`의 `ApiError`와 **같은 클래스 객체**라 어느 이름으로 `instanceof`를 해도 통합니다.
|
|
895
|
+
|
|
896
|
+
### `/util` — 스토어 없이
|
|
897
|
+
|
|
898
|
+
```ts
|
|
899
|
+
import { apiClient, ApiError, setUnauthorizedHandler } from '@yeongseoksong/framework/util'
|
|
900
|
+
|
|
901
|
+
setUnauthorizedHandler(() => myOwnLogout())
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
### 토큰·재발급
|
|
905
|
+
|
|
906
|
+
```ts
|
|
907
|
+
// 쿠키 대신 Authorization 헤더를 쓴다면 (토큰 보관은 앱 몫입니다)
|
|
908
|
+
PlSetAccessTokenGetter(() => myTokenStore.accessToken)
|
|
909
|
+
|
|
910
|
+
// 401 시 재발급. true를 반환하면 실패했던 요청을 1회만 재시도합니다.
|
|
911
|
+
PlSetRefreshHandler(async () => {
|
|
912
|
+
try {
|
|
913
|
+
await PlAxios.post('/auth/refresh')
|
|
914
|
+
return true
|
|
915
|
+
} catch {
|
|
916
|
+
return false
|
|
917
|
+
}
|
|
918
|
+
})
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
재시도는 `_retry` 플래그로 1회에 묶여 무한 루프가 나지 않고, 동시에 여러 요청이 401을 받아도 진행 중인 재발급 promise를 공유해 refresh 엔드포인트는 **한 번만** 호출됩니다.
|
|
922
|
+
|
|
923
|
+
> `/platform`은 스토어를 가져오므로 클라이언트 전용입니다. 서버 컴포넌트·서버 액션에서는 `/util`의 `apiClient`를 쓰세요.
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
865
927
|
## 타입
|
|
866
928
|
|
|
867
929
|
```ts
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as axios from 'axios';
|
|
2
|
+
import { ApiError, ApiErrorBody, setAccessTokenGetter, setRefreshHandler } from '@yeongseoksong/framework/util';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 스토어와 연동된 API 클라이언트 진입점.
|
|
6
|
+
*
|
|
7
|
+
* 인스턴스 자체는 `util/axios.util.ts`가 소유한다 — axios에만 의존하는 순수 유틸이라
|
|
8
|
+
* `/util`로도 그대로 나간다. 여기서는 **재수출**만 하므로 사본이 늘지 않는다:
|
|
9
|
+
* import specifier가 상대 경로가 아니라 자기참조(`@yeongseoksong/framework/util`)이고
|
|
10
|
+
* `tsup.config.ts`의 platform 패스에서 external로 잡혀 있어, esbuild가 인라인 복사하지
|
|
11
|
+
* 않고 import 문으로 남긴다. 상대 경로(`'../util'`)로 바꾸는 순간 `axios.create()`가
|
|
12
|
+
* `dist/util`과 `dist/platform`에서 각각 돌아 인터셉터·토큰 getter 등록이 서로 다른
|
|
13
|
+
* 인스턴스에 걸리는 조용한 버그가 된다.
|
|
14
|
+
*/
|
|
15
|
+
declare const PlAxios: axios.AxiosInstance;
|
|
16
|
+
/** {@link ApiError}의 platform 이름. 같은 클래스 객체라 `instanceof`가 양쪽 이름 모두에서 통한다. */
|
|
17
|
+
declare const PlApiError: typeof ApiError;
|
|
18
|
+
type PlApiError = ApiError;
|
|
19
|
+
/** 서버가 내려주는 표준 에러 바디. */
|
|
20
|
+
type PlApiErrorBody = ApiErrorBody;
|
|
21
|
+
/**
|
|
22
|
+
* 매 요청에 `Authorization: Bearer <token>` 헤더를 붙일 getter를 등록한다.
|
|
23
|
+
* `useAuthStore`는 토큰을 담지 않으므로(`partialize`가 프로필만 저장) 이 경로만은
|
|
24
|
+
* 스토어와 무관하게 소비자가 자신의 저장소를 연결한다.
|
|
25
|
+
*/
|
|
26
|
+
declare const PlSetAccessTokenGetter: typeof setAccessTokenGetter;
|
|
27
|
+
/** 401 시 토큰 재발급을 시도할 핸들러를 등록한다. `true`를 반환하면 실패했던 요청을 1회 재시도한다. */
|
|
28
|
+
declare const PlSetRefreshHandler: typeof setRefreshHandler;
|
|
29
|
+
type PlUnauthorizedHandler = () => void;
|
|
30
|
+
/**
|
|
31
|
+
* 최종 인증 실패(재발급이 없거나 실패한 401) 시 실행할 **추가** 핸들러를 등록한다.
|
|
32
|
+
* `null`을 넘기면 해제된다.
|
|
33
|
+
*
|
|
34
|
+
* 세션 정리(`useAuthStore.logout()`)는 위 인터셉터가 이미 하므로 여기서 또 할 필요가
|
|
35
|
+
* 없다. 이 콜백은 프레임워크가 대신할 수 없는 앱 고유의 후처리 — 로그인 페이지로
|
|
36
|
+
* 라우팅, 토스트, 로깅 — 를 위한 자리다.
|
|
37
|
+
*/
|
|
38
|
+
declare function PlSetUnauthorizedHandler(handler: PlUnauthorizedHandler | null): void;
|
|
39
|
+
|
|
40
|
+
export { PlApiError, type PlApiErrorBody, PlAxios, PlSetAccessTokenGetter, PlSetRefreshHandler, PlSetUnauthorizedHandler };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// platform/axios.ts
|
|
4
|
+
import {
|
|
5
|
+
apiClient,
|
|
6
|
+
ApiError,
|
|
7
|
+
setAccessTokenGetter,
|
|
8
|
+
setRefreshHandler,
|
|
9
|
+
setUnauthorizedHandler
|
|
10
|
+
} from "@yeongseoksong/framework/util";
|
|
11
|
+
import { useAuthStore } from "@yeongseoksong/framework/store";
|
|
12
|
+
var PlAxios = apiClient;
|
|
13
|
+
var PlApiError = ApiError;
|
|
14
|
+
var PlSetAccessTokenGetter = setAccessTokenGetter;
|
|
15
|
+
var PlSetRefreshHandler = setRefreshHandler;
|
|
16
|
+
var appUnauthorizedHandler = null;
|
|
17
|
+
setUnauthorizedHandler(() => {
|
|
18
|
+
useAuthStore.getState().logout();
|
|
19
|
+
appUnauthorizedHandler == null ? void 0 : appUnauthorizedHandler();
|
|
20
|
+
});
|
|
21
|
+
function PlSetUnauthorizedHandler(handler) {
|
|
22
|
+
appUnauthorizedHandler = handler;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
PlApiError,
|
|
26
|
+
PlAxios,
|
|
27
|
+
PlSetAccessTokenGetter,
|
|
28
|
+
PlSetRefreshHandler,
|
|
29
|
+
PlSetUnauthorizedHandler
|
|
30
|
+
};
|