Skip to content
Closed
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
22 changes: 0 additions & 22 deletions src/api/Myplace/saved.ts

This file was deleted.

34 changes: 0 additions & 34 deletions src/api/Myplace/saveg.ts

This file was deleted.

168 changes: 114 additions & 54 deletions src/api/Myplace/savep.api.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,137 @@
import api from "@/api/api";
// src/api/Myplace/savep.api.ts
import api from "@/api/api";

export type SaveMyPlaceRequest = {
contentId: string;
regionName: string;
themeName: string;
cnctrLevel: number;
contentId: string | number;

// 코드 기반(권장)
areaCode?: number | string;
sigunguCode?: number | string;
cat1?: string;
cat2?: string;

cnctrLevel?: number;

// 레거시(문자명) — 백엔드가 아직 받을 수도 있어 optional로 유지
regionName?: string;
themeName?: string;
};

export type SaveMyPlaceResponse = {
placedId: number;
type: string;
like: boolean;
export type SaveMyPlaceData = {
placeId: number;
type: string; // "CREATED" | "UPDATED" | "UNCHANGED" 등 백 정의에 맞춰 들어옴
enabled: boolean;
changed: boolean;
likeCount: number;
message: string;
createdAt: string;
updatedAt: string;
likeCount?: number;
message?: string; // 백에서 사유 메시지 내려줄 수 있음
createdAt?: string;
updatedAt?: string;
};

type ApiEnvelope<T> = {
success?: boolean;
data?: T;
message?: string;
code?: string | number;
type Wrapped<T> = {
success: boolean;
data: T | null;
error?: {
code?: string | number;
code?: number | string;
status?: number;
message?: string;
path?: string;
timestamp?: string | number;
detail?: string;
};
detail?: unknown;
} | null;
};

function unwrapResponse<T>(raw: T | ApiEnvelope<T>): T {
const anyRaw = raw as any;
if (anyRaw && typeof anyRaw === "object" && "data" in anyRaw && anyRaw.data) {
return anyRaw.data as T;
class AppError extends Error {
code: number | string;
status?: number;
raw?: unknown;
constructor(msg: string, code: number | string, status?: number, raw?: unknown) {
super(msg);
this.name = "AppError";
this.code = code;
this.status = status;
this.raw = raw;
}
return raw as T;
}

function assertResponseShape(res: any): asserts res is SaveMyPlaceResponse {
if (!res || typeof res.placedId !== "number") {
throw new Error("서버 응답 형식이 예상과 다릅니다.");
function safeParseJson(x: any) {
try {
if (typeof x === "string") return JSON.parse(x);
return x;
} catch {
return x;
}
}

export async function savePlace(payload: SaveMyPlaceRequest): Promise<SaveMyPlaceResponse> {
try {
const body: SaveMyPlaceRequest = {
...payload,
cnctrLevel: Number(payload.cnctrLevel),
};

const { data } = await api.put<SaveMyPlaceResponse | ApiEnvelope<SaveMyPlaceResponse>>(
"/my/places/save",
body,
{ headers: { "Content-Type": "application/json" } }
);

const unwrapped = unwrapResponse<SaveMyPlaceResponse>(data);
assertResponseShape(unwrapped);
return unwrapped;
} catch (err: any) {
console.error("[savePlace][raw error]", err?.response || err);
const serverMsg =
err?.response?.data?.message ||
err?.response?.data?.error?.message ||
err?.message;
throw new Error(serverMsg || "네트워크 오류 또는 서버 에러가 발생했습니다.");
function throwNormalizedError(e: any): never {
const resp = e?.response;
const data = resp?.data;
const err = data?.error ?? data;

const status = resp?.status ?? err?.status ?? 0;
const code = err?.code ?? status ?? "UNKNOWN";
const msg =
err?.message ||
data?.message ||
e?.message ||
"요청 처리 중 오류가 발생했습니다.";

if (resp) {
console.debug("[saveMyPlace][HTTP ERROR]", {
method: resp.config?.method,
url: resp.config?.url,
status,
requestBody: safeParseJson(resp.config?.data),
responseBody: data,
});
}
throw new AppError(msg, code, status, e);
}

export async function saveMyPlace(body: SaveMyPlaceRequest): Promise<SaveMyPlaceData> {
const payload: SaveMyPlaceRequest = {
...body,
contentId: String(body.contentId),
};

try {
console.debug("[saveMyPlace][REQUEST]", {
method: "PUT",
url: "/my/places/save",
payload,
});

const { data, status, config } = await api.put<SaveMyPlaceData | Wrapped<SaveMyPlaceData>>("my/places/save", payload, {
headers: { "Content-Type": "application/json" },
});

console.debug("[saveMyPlace][RESPONSE]", {
status,
url: config?.url,
raw: data,
});

// 비래핑
if ((data as any)?.placeId !== undefined) {
return data as SaveMyPlaceData;
}

// 래핑
const wrapped = data as Wrapped<SaveMyPlaceData>;
if (typeof wrapped?.success === "boolean") {
if (wrapped.success && wrapped.data) return wrapped.data;

const em = wrapped?.error?.message || "요청이 실패했습니다.";
const ec = wrapped?.error?.code ?? status ?? 400;
const es = wrapped?.error?.status ?? status;
throw new AppError(em, ec, es, wrapped?.error);
}

throw new AppError("알 수 없는 응답 형식입니다.", "UNEXPECTED_RESPONSE", status, data);
} catch (e) {
throwNormalizedError(e);
}
}

export default { savePlace };
// 기존 import 경로 호환
export { saveMyPlace as savePlace };
export { AppError };
77 changes: 0 additions & 77 deletions src/api/Myplace/savep.ts

This file was deleted.

65 changes: 65 additions & 0 deletions src/api/travel/place.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// src/api/travel/place.api.ts
import api from '@/api/api';

export type PlaceListItem = {
contentId: string | number;
title: string;
imageUrl?: string;
likeCount?: number;
areaCode?: number | string;
sigunguCode?: number | string;
cat1?: string;
cat2?: string;
tranquil?: boolean;
// ... 필요한 필드들
};

export type PlacesQuery = {
page?: number;
size?: number;

// === 필터 ===
cat1?: string | null;
cat2?: string[] | null; // 다중 cat2 허용
areaCode?: number | string;
sigunguCode?: number | string;
tranquil?: boolean;
keywords?: string;
};

export async function fetchPlaces(query: PlacesQuery = {}) {
// 서버가 cat2 키를 'cat2List'로 받는다면 아래 키만 바꾸면 됨.
// const CAT2_KEY = 'cat2List';
const CAT2_KEY = 'cat2';

const params: Record<string, any> = { ...query };

if (!query.cat1) delete params.cat1;
if (!query.cat2 || !query.cat2.length) {
delete params.cat2;
} else {
// axios 기본 직렬화는 배열에 [] 붙이거나 인덱싱할 수 있어 호환 이슈 발생
// → URLSearchParams로 'cat2=...&cat2=...' 형태 강제
}

// 커스텀 직렬화: cat2 배열은 동일 키 반복으로 직렬화
const res = await api.get<PlaceListItem[]>('/places', {
params,
paramsSerializer: (p) => {
const usp = new URLSearchParams();
Object.entries(p).forEach(([k, v]) => {
if (v === undefined || v === null || v === '') return;
if (k === 'cat2' && Array.isArray(v)) {
v.forEach((vv) => usp.append(CAT2_KEY, String(vv)));
} else if (Array.isArray(v)) {
v.forEach((vv) => usp.append(k, String(vv)));
} else {
usp.append(k, String(v));
}
});
return usp.toString();
},
});

return res.data;
}
Loading
Loading