{"version":3,"sources":["../src/api-hooks/configure.ts","../src/api-hooks/use-api-query.ts","../src/api-hooks/use-api-mutation.ts","../src/api-hooks/create-api-error-handler.ts","../src/api-hooks/handle-api-err.tsx"],"names":["axios","handleApiErr"],"mappings":"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","file":"api-hooks.mjs","sourcesContent":["/**\n * configure — @linkup/api-hooks 전역 설정 주입\n *\n * 앱 최상단(providers, _app.tsx 등)에서 한 번만 호출한다.\n *\n * @example\n * configure({\n *   axios: myAxiosInstance,\n *   onError: (err) => toast.error(getErrorMessage(err)),\n *   onPendingChange: (delta) => setPendingCount((c) => c + delta),\n *   extractData: (res) => res.data.data,\n * });\n */\nimport type { AxiosInstance, AxiosResponse } from \"axios\";\n\nexport interface ApiHooksConfig {\n    /** 프로젝트에서 사용하는 axios 인스턴스 (인터셉터 포함) */\n    axios: AxiosInstance;\n    /**\n     * API 에러 발생 시 호출되는 핸들러.\n     * toast, 로깅 등을 여기서 처리한다.\n     */\n    onError?: (error: unknown) => void;\n    /**\n     * 뮤테이션 pending 상태 변화 콜백.\n     * delta 1 = 요청 시작, -1 = 요청 종료.\n     * 전역 로딩 스피너 등에 활용한다.\n     */\n    onPendingChange?: (delta: 1 | -1) => void;\n    /**\n     * axios 응답에서 실제 데이터를 추출하는 함수.\n     * 기본값: (res) => res.data.data\n     *\n     * @example // { code, message, data } 구조일 때\n     * extractData: (res) => res.data.data\n     *\n     * @example // { result } 구조일 때\n     * extractData: (res) => res.data.result\n     */\n    extractData?: <T>(response: AxiosResponse) => T;\n}\n\nlet _config: ApiHooksConfig | null = null;\n\n/** 전역 설정 등록. 앱 초기화 시 한 번만 호출한다. */\nexport function configure(config: ApiHooksConfig): void {\n    _config = config;\n}\n\n/** 내부에서 설정을 가져온다. configure() 미호출 시 오류. */\nexport function getConfig(): ApiHooksConfig {\n    if (!_config) {\n        throw new Error(\n            \"[api-hooks] configure() 를 먼저 호출해야 합니다.\\n\" +\n            \"앱 최상단에서 configure({ axios, onError, ... })를 실행하세요.\"\n        );\n    }\n    return _config;\n}\n\n/** 기본 응답 추출 함수 */\nexport function defaultExtractData<T>(res: AxiosResponse): T {\n    return res.data.data as T;\n}\n","/**\n * useApiQuery — GET 요청 공통 훅 (React Query useQuery 래퍼)\n *\n * 기본 동작:\n * - 윈도우 포커스 시 자동 재요청\n * - 실패 시 재시도 없음\n * - 이전 데이터 유지 (placeholderData)\n *\n * @example // 기본\n * const { data, isLoading } = useApiQuery<Member[]>({\n *   queryKey: [\"members\"],\n *   url: \"/api/members\",\n * });\n *\n * @example // 파라미터 + 타입 변환(select)\n * const { data } = useApiQuery<MemberListResponse, Member[]>({\n *   queryKey: [\"members\", page],\n *   url: \"/api/members\",\n *   params: { page, size: 20 },\n *   options: {\n *     select: (res) => res.list,\n *     enabled: !!userId,\n *   },\n * });\n */\nimport {\n    useQuery,\n    type QueryKey,\n    type UseQueryOptions,\n    type UseQueryResult,\n} from \"@tanstack/react-query\";\n\nimport { defaultExtractData, getConfig } from \"./configure\";\n\nexport interface UseApiQueryProps<T, S = T> {\n    /** React Query 캐시 키 */\n    queryKey: QueryKey;\n    /** GET 요청 URL */\n    url: string;\n    /** 쿼리 파라미터 (?key=value) */\n    params?: Record<string, unknown>;\n    /** useQuery 추가 옵션 (queryKey/queryFn 제외) */\n    options?: Omit<UseQueryOptions<T, Error, S>, \"queryKey\" | \"queryFn\">;\n}\n\nexport function useApiQuery<T, S = T>({\n    queryKey,\n    url,\n    params,\n    options,\n}: UseApiQueryProps<T, S>): UseQueryResult<S, Error> {\n    const { axios, extractData = defaultExtractData } = getConfig();\n\n    return useQuery<T, Error, S>({\n        queryKey,\n        queryFn: async () => {\n            const res = await axios.get(url, { params });\n            return extractData<T>(res);\n        },\n        refetchOnWindowFocus: true,\n        retry: false,\n        // 새 데이터 로딩 중에도 이전 데이터를 유지해 레이아웃 깜빡임을 방지\n        placeholderData: (prev) => prev,\n        ...options,\n    });\n}\n","/**\n * useApiMutation — POST/PATCH/PUT/DELETE 요청 공통 훅 (React Query useMutation 래퍼)\n *\n * 주요 기능:\n * - 동일 URL+Method 중복 요청 자동 차단 (inFlight)\n * - 요청 시작/종료 시 onPendingChange 콜백 (전역 로딩 스피너 연동)\n * - 성공 후 refreshKeys 자동 invalidate\n * - 에러 발생 시 onError 콜백 (configure에서 설정한 핸들러 호출)\n *\n * 호출 방식 A — 훅에 url/method 고정:\n * @example\n * const mutation = useApiMutation({ url: \"/api/members\", method: \"post\" });\n * mutation.mutate({ name: \"홍길동\", email: \"hong@link-up.kr\" });\n *\n * 호출 방식 B — mutate 시점에 url/method 결정 (url/method 생략):\n * @example\n * const mutation = useApiMutation({});\n * mutation.mutate({ url: \"/api/members/1\", method: \"patch\", data: { name: \"홍길동\" } });\n *\n * refreshKeys 사용:\n * @example\n * const mutation = useApiMutation({\n *   url: \"/api/members\",\n *   method: \"post\",\n *   refreshKeys: [\"members\", \"members-stats\"],\n * });\n */\nimport {\n    useMutation,\n    useQueryClient,\n    type UseMutationOptions,\n    type UseMutationResult,\n} from \"@tanstack/react-query\";\n\nimport { getConfig } from \"./configure\";\n\nexport type HttpMethod = \"post\" | \"patch\" | \"put\" | \"delete\";\n\n/**\n * mutate() 에 url/method/data 를 직접 넘기는 동적 변수 형태.\n * 훅에 url/method 를 고정하지 않을 때 사용한다.\n */\nexport interface DynamicMutationVariables<TData = unknown> {\n    url: string;\n    method: HttpMethod;\n    data?: TData;\n    /** 이 mutate 호출에만 적용할 캐시 무효화 키 (훅의 refreshKeys 보다 우선) */\n    refreshKeys?: string[];\n}\n\nexport interface UseApiMutationProps<TVariables = unknown, TResult = unknown> {\n    /** 요청 URL. 훅 레벨에서 고정할 때 사용. */\n    url?: string;\n    /** HTTP 메서드. 훅 레벨에서 고정할 때 사용. */\n    method?: HttpMethod;\n    /**\n     * 성공 후 무효화할 React Query 키 목록 (문자열 배열).\n     * @example refreshKeys={[\"members\", \"members-stats\"]}\n     */\n    refreshKeys?: string[];\n    /** 성공 콜백 (onSuccess) */\n    onSuccess?: (data: TResult, variables: TVariables) => void;\n    /** 에러 콜백 — configure 의 onError 에 추가로 호출된다 */\n    onError?: (error: Error, variables: TVariables) => void;\n    /** useMutation 나머지 옵션 (mutationFn / onSuccess / onError 제외) */\n    options?: Omit<\n        UseMutationOptions<TResult, Error, TVariables>,\n        \"mutationFn\" | \"onSuccess\" | \"onError\"\n    >;\n}\n\n/** 동적 변수 형태인지 판별 */\nfunction isDynamic(v: unknown): v is DynamicMutationVariables {\n    return (\n        typeof v === \"object\" &&\n        v !== null &&\n        \"url\" in v &&\n        \"method\" in v\n    );\n}\n\n/** 중복 요청 추적 */\nconst inFlight = new Set<string>();\nconst makeKey = (method: string, url: string) =>\n    `${method.toUpperCase()}:${url}`;\n\nexport function useApiMutation<TVariables = unknown, TResult = unknown>({\n    url: hookUrl,\n    method: hookMethod,\n    refreshKeys: hookRefreshKeys,\n    onSuccess: externalOnSuccess,\n    onError: externalOnError,\n    options,\n}: UseApiMutationProps<TVariables, TResult>): UseMutationResult<\n    TResult,\n    Error,\n    TVariables\n> {\n    const queryClient = useQueryClient();\n    const { axios, onError, onPendingChange } = getConfig();\n\n    return useMutation<TResult, Error, TVariables>({\n        ...options,\n        mutationFn: async (variables) => {\n            // 동적 변수(url/method 포함) vs 단순 payload 판별\n            const dynamic = isDynamic(variables);\n            const targetUrl = dynamic ? variables.url : hookUrl;\n            const targetMethod = dynamic ? variables.method : hookMethod;\n            const payload = dynamic ? variables.data : variables;\n\n            if (!targetUrl || !targetMethod) {\n                throw new Error(\n                    \"[api-hooks] url 과 method 가 필요합니다. \" +\n                    \"훅 인자 또는 mutate() 인자로 전달하세요.\"\n                );\n            }\n\n            const key = makeKey(targetMethod, targetUrl);\n\n            // 동일 요청 중복 차단\n            if (inFlight.has(key)) {\n                throw new Error(\"처리 중인 요청이 있습니다. 잠시 후 다시 시도하세요.\");\n            }\n\n            // 요청 시작 등록 (pending 증가는 try 진입 전)\n            inFlight.add(key);\n            onPendingChange?.(1);\n\n            try {\n                const res =\n                    targetMethod === \"delete\"\n                        ? await axios.delete(targetUrl, { data: payload })\n                        : await axios[targetMethod](targetUrl, payload);\n\n                return res.data as TResult;\n            } finally {\n                inFlight.delete(key);\n                onPendingChange?.(-1);\n            }\n        },\n\n        onSuccess: (data, variables, mutation) => {\n            // 캐시 무효화: mutate 시점 변수의 refreshKeys > 훅의 refreshKeys\n            const dynamic = isDynamic(variables);\n            const keys =\n                (dynamic ? variables.refreshKeys : undefined) ?? hookRefreshKeys;\n\n            if (keys?.length) {\n                keys.forEach((key) =>\n                    queryClient.invalidateQueries({ queryKey: [key], exact: false })\n                );\n            }\n\n            externalOnSuccess?.(data, variables);\n        },\n\n        onError: (error, variables) => {\n            if (process.env.NODE_ENV === \"development\") {\n                console.error(\"[api-hooks] mutation error:\", error);\n            }\n            onError?.(error);\n            externalOnError?.(error as Error, variables);\n        },\n    });\n}\n","/**\n * createApiErrorHandler — 클라이언트 공통 API 에러 핸들러 팩토리\n *\n * configure()의 onError 콜백으로 주입해 useApiMutation 에러를 전역 처리한다.\n * toast, redirect 등 프로젝트별 로직은 옵션으로 주입한다.\n *\n * 서버 응답 형식: { detail?: string, message?: string, fieldErrors?: unknown[] }\n * fieldErrors가 있으면(zod 검증 에러) message를 우선 추출하고,\n * 없으면 detail을 우선 추출한다(그 다음 message).\n *\n * @example\n * // providers.tsx\n * import { createApiErrorHandler, configure } from \"@sunkim4638/admin-modules/api-hooks\";\n * import { toast } from \"sonner\";\n *\n * const handleApiErr = createApiErrorHandler({\n *   onMessage: (msg) => toast.error(msg),\n *   onUnauthorized: () => window.location.href = \"/login\",\n * });\n *\n * configure({ axios: ax, onError: handleApiErr });\n */\n\nimport axios from \"axios\";\n\nexport interface ApiErrorHandlerOptions {\n    /**\n     * 에러 메시지를 표시할 함수.\n     * toast.error, alert 등 프로젝트에 맞게 주입한다.\n     * 기본: console.error\n     */\n    onMessage?: (message: string) => void;\n    /** 401 Unauthorized 처리 — 기본: 아무것도 안 함 */\n    onUnauthorized?: () => void;\n    /** 403 Forbidden 처리 — 기본: onMessage로 \"권한이 없습니다.\" 표시 */\n    onForbidden?: () => void;\n    /**\n     * 서버 응답에서 메시지를 추출하는 함수\n     * 기본: data.detail → data.message 순으로 추출\n     */\n    extractMessage?: (data: any) => string | undefined;\n    /** 개발 환경에서 console.error 출력 여부 — 기본: true */\n    debug?: boolean;\n}\n\nexport function createApiErrorHandler(options: ApiErrorHandlerOptions = {}) {\n    const {\n        onMessage,\n        onUnauthorized,\n        onForbidden,\n        extractMessage = (data) =>\n            data?.fieldErrors ? data?.message : data?.detail || data?.message,\n        debug = true,\n    } = options;\n\n    const showMessage = onMessage ?? ((msg: string) => console.error(\"[api-error]\", msg));\n\n    return function handleApiErr(error: unknown): void {\n        if (debug && process.env.NODE_ENV === \"development\") {\n            console.error(\"[api-error]\", error);\n        }\n\n        if (!axios.isAxiosError(error)) {\n            showMessage(error instanceof Error ? error.message : \"알 수 없는 오류가 발생했습니다.\");\n            return;\n        }\n\n        const status = error.response?.status;\n        const message =\n            extractMessage(error.response?.data) ??\n            `API 오류 (${status ?? \"unknown\"})`;\n\n        if (status === 401) {\n            onUnauthorized?.();\n            return;\n        }\n\n        if (status === 403) {\n            if (onForbidden) {\n                onForbidden();\n            } else {\n                showMessage(\"권한이 없습니다.\");\n            }\n            return;\n        }\n\n        showMessage(message);\n    };\n}\n","/**\n * handleApiErr — sonner toast를 사용한 공통 API 에러 핸들러\n *\n * configure()의 onError에 직접 전달한다.\n * 401 처리는 axios interceptor에서 하고 여기선 메시지 표시만 담당한다.\n *\n * @example\n * // providers.tsx\n * import { handleApiErr, configure } from \"@sunkim4638/admin-modules/api-hooks\";\n *\n * configure({ axios: ax, onError: handleApiErr });\n */\n\n\"use client\";\n\nimport { toast } from \"sonner\";\nimport { createApiErrorHandler } from \"./create-api-error-handler\";\n\nfunction toastMessage(msg: string) {\n    // 줄바꿈이 있으면 각 줄을 div로 분리\n    if (!msg.includes(\"\\n\")) {\n        toast.error(msg);\n        return;\n    }\n    toast.error(\n        <div>\n            {msg.split(\"\\n\").map((line, i) => (\n                <div key={i}>{line}</div>\n            ))}\n        </div>\n    );\n}\n\nexport const handleApiErr = createApiErrorHandler({\n    onMessage: toastMessage,\n    onForbidden: () => toastMessage(\"권한이 없습니다.\"),\n    // onUnauthorized 미설정 → 401은 axios interceptor에서 처리\n});\n"]}