@yeongseoksong/framework 1.6.1 → 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 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
+ };
@@ -930,25 +930,27 @@ declare function HeroCarousel({ slides, children }: HeroCarouselProps): react_js
930
930
  interface HeaderProps {
931
931
  navItems: NavItem[];
932
932
  loginFlag?: boolean;
933
+ /** 로고 크기. 미지정 시 Logo의 theme 기본값(`md`)을 그대로 따른다. */
934
+ logoSize?: MantineSize;
933
935
  }
934
936
  /**
935
937
  * Mega 변형(기본) — 헤더 전체가 아래로 확장되며 하위 링크가 상위 항목 아래 컬럼으로
936
938
  * 한꺼번에 노출되는 메가 메뉴.
937
939
  */
938
- declare function MegaHeader({ navItems, loginFlag }: HeaderProps): react_jsx_runtime.JSX.Element;
940
+ declare function MegaHeader({ navItems, loginFlag, logoSize }: HeaderProps): react_jsx_runtime.JSX.Element;
939
941
  /**
940
942
  * Simple 변형 — 헤더 바 높이는 고정이고, 자식을 가진 상위 항목마다
941
943
  * Mantine `Menu`가 하나씩 붙어 개별 드롭다운으로 열린다.
942
944
  * 바가 자라지 않으므로 AppShell.Header 높이를 그대로 쓰는 레이아웃에 적합하다.
943
945
  */
944
- declare function SimpleHeader({ navItems, loginFlag }: HeaderProps): react_jsx_runtime.JSX.Element;
946
+ declare function SimpleHeader({ navItems, loginFlag, logoSize }: HeaderProps): react_jsx_runtime.JSX.Element;
945
947
  /**
946
948
  * Panel 변형 — Simple처럼 바 높이는 60px로 고정하고 상위 항목마다 Mantine `Menu`가 붙지만,
947
949
  * 드롭다운 내부는 Mega와 동일한 그룹 레이아웃으로 편다: 손자가 없는 자식은 단일 링크,
948
950
  * 손자가 있는 자식은 그룹 제목(`SdLink.Body`) 아래로 손자를 `SdLink.Sub`로 얹는다.
949
951
  * Simple의 개별 드롭다운 안정성 + Mega의 풍부한 그룹 구성이 함께 필요할 때 쓴다.
950
952
  */
951
- declare function PanelHeader({ navItems, loginFlag }: HeaderProps): react_jsx_runtime.JSX.Element;
953
+ declare function PanelHeader({ navItems, loginFlag, logoSize }: HeaderProps): react_jsx_runtime.JSX.Element;
952
954
  declare const SdHeader: typeof MegaHeader & {
953
955
  Mega: typeof MegaHeader;
954
956
  Simple: typeof SimpleHeader;
@@ -963,8 +965,10 @@ interface SdFooterProps {
963
965
  navItems?: NavItem[];
964
966
  policyLinks?: NavItem[];
965
967
  description?: string;
968
+ /** 로고 크기. 기본 `sm`. */
969
+ logoSize?: MantineSize;
966
970
  }
967
- declare function SdFooter({ company, navItems, policyLinks, description }: SdFooterProps): react_jsx_runtime.JSX.Element;
971
+ declare function SdFooter({ company, navItems, policyLinks, description, logoSize }: SdFooterProps): react_jsx_runtime.JSX.Element;
968
972
 
969
973
  interface SdFeatureSectionProps extends Omit<BoxProps, 'title'> {
970
974
  label?: string;
@@ -1052,13 +1056,15 @@ interface SdLoginViewProps {
1052
1056
  children?: ReactNode;
1053
1057
  /** 화면 최소 높이. 카탈로그처럼 좁은 프레임에 담을 때 줄인다. */
1054
1058
  mih?: string | number;
1059
+ /** 로고 크기. 기본 `sm`. */
1060
+ logoSize?: MantineSize;
1055
1061
  }
1056
- declare function Card({ title, description, mih, ...formProps }: SdLoginViewProps): react_jsx_runtime.JSX.Element;
1062
+ declare function Card({ title, description, mih, logoSize, ...formProps }: SdLoginViewProps): react_jsx_runtime.JSX.Element;
1057
1063
  interface SdLoginSplitProps extends SdLoginViewProps {
1058
1064
  brandTitle?: ReactNode;
1059
1065
  brandDescription?: ReactNode;
1060
1066
  }
1061
- declare function Split({ title, description, brandTitle, brandDescription, mih, ...formProps }: SdLoginSplitProps): react_jsx_runtime.JSX.Element;
1067
+ declare function Split({ title, description, brandTitle, brandDescription, mih, logoSize, ...formProps }: SdLoginSplitProps): react_jsx_runtime.JSX.Element;
1062
1068
  declare const SdLoginView: {
1063
1069
  /** 중앙 정렬 카드 — 기본 로그인 화면 */
1064
1070
  Card: typeof Card;
@@ -1096,13 +1102,16 @@ declare const SdResultError: ({ title, description, primaryAction, secondaryActi
1096
1102
 
1097
1103
  /** 헤더 변형 선택자 — SdHeader의 네임스페이스 키와 1:1로 대응한다. */
1098
1104
  type HeaderVariant = 'mega' | 'simple' | 'panel';
1099
- declare function MainLayout({ children, navItems, companyInfo, loginFlag, headerVariant, }: {
1105
+ declare function MainLayout({ children, navItems, companyInfo, loginFlag, headerVariant, headerLogoSize, footerLogoSize, }: {
1100
1106
  children: React.ReactNode;
1101
1107
  navItems: NavItem[];
1102
1108
  companyInfo: CompanyInfo;
1103
1109
  loginFlag?: boolean;
1104
1110
  /** 기본 `mega` — hover 시 헤더가 확장되는 메가 메뉴. `simple`은 바 높이 고정 + 개별 드롭다운. `panel`은 바 높이 고정 + 드롭다운 내부를 Mega식 그룹 컬럼으로. */
1105
1111
  headerVariant?: HeaderVariant;
1112
+ /** 헤더 로고 크기. 미지정 시 Logo의 theme 기본값(`md`)을 그대로 따른다. */
1113
+ headerLogoSize?: MantineSize;
1114
+ footerLogoSize?: MantineSize;
1106
1115
  }): react_jsx_runtime.JSX.Element;
1107
1116
 
1108
1117
  /**