Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# swagger openapi.json 파일
/openapi.json
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"lint:fix": "pnpm lint --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prepare": "husky"
"prepare": "husky",
"openapi:pull": "curl --fail --show-error --location -s \"$SUPABASE_URL/rest/v1/\" -H \"apikey: $SUPABASE_ANON_KEY\" -H \"Authorization: Bearer $SUPABASE_ANON_KEY\" -H \"Accept: application/openapi+json\" -o openapi.json",
"openapi:fix": "jq 'del(.components.parameters.preferParams.schema.enum?, .parameters.preferParams.enum?) | def walk(f): . as $in | if type==\"object\" then reduce keys[] as $k ({}; . + { ($k): ($in[$k] | walk(f)) }) | f elif type==\"array\" then map(walk(f)) | f else f end; walk(if (type==\"object\" and has(\"enum\") and (.enum|type)==\"array\" and (.enum|length)==0) then del(.enum) else . end)' openapi.json > openapi.json.tmp && mv openapi.json.tmp openapi.json",
"openapi:watch": "npx swagger-ui-watcher ./openapi.json"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
Expand All @@ -27,6 +30,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@supabase/supabase-js": "^2.75.0",
"@tanstack/react-query": "^5.90.2",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
Expand Down
102 changes: 102 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createClient } from "@supabase/supabase-js";
import axios, { AxiosHeaders } from "axios";

// 1) Supabase Auth 전용 클라이언트(세션/토큰 자동 관리)
export const supabaseAuth = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ auth: { persistSession: true, autoRefreshToken: true } },
);

// 2) Supabase REST 호출용 axios
const baseURL = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1`;
const timeout = Number(process.env.NEXT_PUBLIC_API_TIMEOUT || 10000);

export const api = axios.create({
baseURL,
timeout,
headers: new AxiosHeaders({ "Content-Type": "application/json" }),
});

// 3) 요청 인터셉터: apikey + (로그인 시) Authorization 자동 주입
api.interceptors.request.use(async (config) => {
const anon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const {
data: { session },
} = await supabaseAuth.auth.getSession();
const access = session?.access_token;

const headers = AxiosHeaders.from(config.headers);
headers.set("apikey", anon);
if (access) headers.set("Authorization", `Bearer ${access}`);

// 필요한 기본값이 있다면 여기서 추가로:
// headers.set("Accept", "application/json");

config.headers = headers;
return config;
});

// 4) 응답/에러 표준화
api.interceptors.response.use(
(res) => res,
(err) => {
const status = err?.response?.status;
const message = err?.response?.data?.message || err?.message || "Unexpected error";
return Promise.reject({ status, message, raw: err });
},
);
55 changes: 55 additions & 0 deletions src/api/dashboard_layouts/dashboard_layout.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type {
GetDashboardLayoutsParams,
PatchDashboardLayoutPayloadType,
PostDashboardLayoutPayloadType,
} from "./dashboard_layout.schema";
import { DashboardLayoutService } from "./dashboard_layout.service";

const service = new DashboardLayoutService();

/** 캐시 키 */
export const dashboardLayoutKeys = {
all: () => ["dashboard_layouts"] as const,
list: (params?: GetDashboardLayoutsParams["params"]) =>
[...dashboardLayoutKeys.all(), "list", params ?? {}] as const,
};

/** 쿼리 옵션 (params로 select/order 등 전달 가능) */
export const dashboardLayoutQuery = {
list: (params?: GetDashboardLayoutsParams["params"]) =>
queryOptions({
queryKey: dashboardLayoutKeys.list(params),
queryFn: () => service.getList({ params }),
staleTime: 60_000,
}),
};

/** 훅 */
export function useDashboardLayoutsQuery(params?: GetDashboardLayoutsParams["params"]) {
return useQuery(dashboardLayoutQuery.list(params));
}

export function useCreateDashboardLayoutMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (payload: PostDashboardLayoutPayloadType) => service.post(payload),
onSuccess: () => qc.invalidateQueries({ queryKey: dashboardLayoutKeys.all() }),
});
}

export function usePatchDashboardLayoutMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (payload: PatchDashboardLayoutPayloadType) => service.patch(payload),
onSuccess: () => qc.invalidateQueries({ queryKey: dashboardLayoutKeys.all() }),
});
}

export function useDeleteDashboardLayoutMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => service.delete(id),
onSuccess: () => qc.invalidateQueries({ queryKey: dashboardLayoutKeys.all() }),
});
}
29 changes: 29 additions & 0 deletions src/api/dashboard_layouts/dashboard_layout.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type {
DashboardLayout,
DashboardLayoutInsert,
DashboardLayoutUpdate,
} from "../types/domain";

/** 서버 응답 타입 */
export type GetDashboardLayoutsResultType = DashboardLayout[];
export type PostDashboardLayoutResultType = DashboardLayout;
export type PatchDashboardLayoutResultType = DashboardLayout;
export type DeleteDashboardLayoutResultType = void;

/** 요청 DTO (database.d.ts 기준으로만 결정) */
export interface GetDashboardLayoutsParams {
/** 필요 시 외부에서 select/order/filter 등 전달 */
params?: Record<string, string | number | boolean | null | undefined>;
}

export interface PostDashboardLayoutPayloadType {
/** ← Insert 타입 그대로 (필수/옵셔널은 database.d.ts가 결정) */
body: DashboardLayoutInsert;
}

export interface PatchDashboardLayoutPayloadType {
/** ← id 타입도 database.d.ts에서 가져옴 */
id: DashboardLayout["id"];
/** ← Update 타입 그대로 */
body: DashboardLayoutUpdate;
}
Loading