1940 lines
53 KiB
TypeScript
1940 lines
53 KiB
TypeScript
import axios from 'axios';
|
|
import { Car, CarListResponse, CarMaker, CarModel, User, HeroBanner, HeroBannerSettings, CarView } from '@/types';
|
|
|
|
// When NEXT_PUBLIC_API_URL is empty, use relative path (for HTTPS proxy setup)
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
const BASE_URL = API_URL ? `${API_URL}/api` : '/api';
|
|
|
|
const api = axios.create({
|
|
baseURL: BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Add auth token to requests
|
|
api.interceptors.request.use((config) => {
|
|
if (typeof window !== 'undefined') {
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// Cars API
|
|
export const carsApi = {
|
|
getList: async (params: {
|
|
page?: number;
|
|
page_size?: number;
|
|
maker_id?: number;
|
|
model_id?: number;
|
|
year_min?: number;
|
|
year_max?: number;
|
|
price_min?: number;
|
|
price_max?: number;
|
|
mileage_max?: number;
|
|
fuel?: string;
|
|
admin?: boolean;
|
|
is_displayed?: boolean;
|
|
}): Promise<CarListResponse> => {
|
|
const { data } = await api.get('/cars', { params });
|
|
return data;
|
|
},
|
|
|
|
getById: async (id: number, admin?: boolean): Promise<Car> => {
|
|
const { data } = await api.get(`/cars/${id}`, { params: { admin } });
|
|
return data;
|
|
},
|
|
|
|
update: async (id: number, updateData: {
|
|
margin_krw?: number;
|
|
is_displayed?: boolean;
|
|
car_name?: string;
|
|
status?: string;
|
|
}): Promise<Car> => {
|
|
const { data } = await api.put(`/cars/${id}`, updateData);
|
|
return data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/cars/${id}`);
|
|
},
|
|
|
|
getMakers: async (): Promise<CarMaker[]> => {
|
|
const { data } = await api.get('/cars/makers/');
|
|
return data;
|
|
},
|
|
|
|
getModels: async (makerId?: number): Promise<CarModel[]> => {
|
|
const params = makerId ? { maker_id: makerId } : {};
|
|
const { data } = await api.get('/cars/models/', { params });
|
|
return data;
|
|
},
|
|
|
|
// Soldout APIs
|
|
markSoldout: async (carId: number): Promise<{ car_id: number; soldout: boolean; message: string }> => {
|
|
const { data } = await api.post(`/cars/${carId}/soldout`);
|
|
return data;
|
|
},
|
|
|
|
markAvailable: async (carId: number): Promise<{ car_id: number; soldout: boolean; message: string }> => {
|
|
const { data } = await api.delete(`/cars/${carId}/soldout`);
|
|
return data;
|
|
},
|
|
|
|
getSoldoutStats: async (): Promise<{ total_active: number; soldout: number; available: number; soldout_percentage: number }> => {
|
|
const { data } = await api.get('/cars/admin/soldout-stats');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Auth API
|
|
export const authApi = {
|
|
login: async (email: string, password: string): Promise<{ access_token: string }> => {
|
|
const params = new URLSearchParams();
|
|
params.append('username', email);
|
|
params.append('password', password);
|
|
|
|
const { data } = await api.post('/auth/login', params, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
});
|
|
return data;
|
|
},
|
|
|
|
register: async (userData: {
|
|
email: string;
|
|
password: string;
|
|
name?: string;
|
|
phone?: string;
|
|
country?: string;
|
|
}): Promise<User> => {
|
|
const { data } = await api.post('/auth/register', userData);
|
|
return data;
|
|
},
|
|
|
|
getMe: async (): Promise<User> => {
|
|
const { data } = await api.get('/auth/me');
|
|
return data;
|
|
},
|
|
|
|
// 사용자 탈퇴 요청
|
|
requestWithdrawal: async (password: string, reason?: string): Promise<{
|
|
message: string;
|
|
withdrawal_requested_at: string;
|
|
}> => {
|
|
const { data } = await api.post('/auth/withdraw', { password, reason });
|
|
return data;
|
|
},
|
|
|
|
// 탈퇴 요청 취소
|
|
cancelWithdrawal: async (): Promise<{ message: string }> => {
|
|
const { data } = await api.post('/auth/withdraw/cancel');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Inquiries API
|
|
export const inquiriesApi = {
|
|
create: async (carId: number, message: string) => {
|
|
const { data } = await api.post('/inquiries', { car_id: carId, message });
|
|
return data;
|
|
},
|
|
|
|
getList: async () => {
|
|
const { data } = await api.get('/inquiries');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Hero Banners API
|
|
export const heroBannersApi = {
|
|
// Public APIs
|
|
getList: async (lang: string = 'en'): Promise<HeroBanner[]> => {
|
|
const { data } = await api.get('/hero-banners/', { params: { lang } });
|
|
return data;
|
|
},
|
|
|
|
getSettings: async (): Promise<HeroBannerSettings> => {
|
|
const { data } = await api.get('/hero-banners/settings');
|
|
return data;
|
|
},
|
|
|
|
// 차량이 Banner에 연결되어 있는지 확인 (샘플 차량 여부)
|
|
checkBannerCar: async (carId: number): Promise<{ car_id: number; is_banner_car: boolean; banner_id: number | null }> => {
|
|
const { data } = await api.get(`/hero-banners/check-car/${carId}`);
|
|
return data;
|
|
},
|
|
|
|
// Admin APIs
|
|
adminGetList: async (): Promise<HeroBanner[]> => {
|
|
const { data } = await api.get('/hero-banners/admin/list');
|
|
return data;
|
|
},
|
|
|
|
adminGetById: async (id: number): Promise<HeroBanner> => {
|
|
const { data } = await api.get(`/hero-banners/admin/${id}`);
|
|
return data;
|
|
},
|
|
|
|
adminCreate: async (bannerData: Partial<HeroBanner>): Promise<HeroBanner> => {
|
|
const { data } = await api.post('/hero-banners/admin', bannerData);
|
|
return data;
|
|
},
|
|
|
|
adminUpdate: async (id: number, bannerData: Partial<HeroBanner>): Promise<HeroBanner> => {
|
|
const { data } = await api.put(`/hero-banners/admin/${id}`, bannerData);
|
|
return data;
|
|
},
|
|
|
|
adminDelete: async (id: number): Promise<void> => {
|
|
await api.delete(`/hero-banners/admin/${id}`);
|
|
},
|
|
|
|
adminUploadImage: async (file: File): Promise<{ image_url: string }> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const { data } = await api.post('/hero-banners/admin/upload-image', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
return data;
|
|
},
|
|
|
|
adminUpdateSettings: async (settings: Partial<HeroBannerSettings>): Promise<HeroBannerSettings> => {
|
|
const { data } = await api.put('/hero-banners/admin/settings', settings);
|
|
return data;
|
|
},
|
|
|
|
// Banner Toggle & Ordering
|
|
adminToggleBanner: async (carId: number): Promise<{ car_id: number; is_banner: boolean; banner_id?: number; message: string }> => {
|
|
const { data } = await api.post(`/hero-banners/admin/toggle/${carId}`);
|
|
return data;
|
|
},
|
|
|
|
adminReorderBanners: async (carIds: number[]): Promise<{ message: string; count: number }> => {
|
|
const { data } = await api.put('/hero-banners/admin/reorder', { car_ids: carIds });
|
|
return data;
|
|
},
|
|
|
|
adminGetBannerCars: async (): Promise<{ car_ids: number[]; count: number }> => {
|
|
const { data } = await api.get('/hero-banners/admin/banner-cars');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Translations API
|
|
export interface Translation {
|
|
id: number;
|
|
source_text: string;
|
|
category: string;
|
|
text_en?: string;
|
|
text_mn?: string;
|
|
text_ru?: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface TranslationListResponse {
|
|
total: number;
|
|
page: number;
|
|
page_size: number;
|
|
translations: Translation[];
|
|
}
|
|
|
|
export const translationsApi = {
|
|
getCategories: async (): Promise<string[]> => {
|
|
const { data } = await api.get('/translations/categories');
|
|
return data;
|
|
},
|
|
|
|
getList: async (params: {
|
|
page?: number;
|
|
page_size?: number;
|
|
category?: string;
|
|
search?: string;
|
|
}): Promise<TranslationListResponse> => {
|
|
const { data } = await api.get('/translations', { params });
|
|
return data;
|
|
},
|
|
|
|
getById: async (id: number): Promise<Translation> => {
|
|
const { data } = await api.get(`/translations/${id}`);
|
|
return data;
|
|
},
|
|
|
|
create: async (translationData: {
|
|
source_text: string;
|
|
category: string;
|
|
text_en?: string;
|
|
text_mn?: string;
|
|
text_ru?: string;
|
|
}): Promise<Translation> => {
|
|
const { data } = await api.post('/translations', translationData);
|
|
return data;
|
|
},
|
|
|
|
update: async (id: number, translationData: {
|
|
source_text?: string;
|
|
category?: string;
|
|
text_en?: string;
|
|
text_mn?: string;
|
|
text_ru?: string;
|
|
}): Promise<Translation> => {
|
|
const { data } = await api.put(`/translations/${id}`, translationData);
|
|
return data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/translations/${id}`);
|
|
},
|
|
|
|
autoExtract: async (): Promise<{ message: string }> => {
|
|
const { data } = await api.post('/translations/auto-extract');
|
|
return data;
|
|
},
|
|
|
|
seedAllDefaults: async (): Promise<{ message: string; added: number; skipped: number; categories: string[] }> => {
|
|
const { data } = await api.post('/translations/seed-all-defaults');
|
|
return data;
|
|
},
|
|
|
|
bulkLookup: async (texts: string[], lang: string, category?: string): Promise<{ translations: Record<string, string> }> => {
|
|
const { data } = await api.post('/translations/bulk-lookup', {
|
|
texts,
|
|
lang,
|
|
category,
|
|
});
|
|
return data;
|
|
},
|
|
|
|
// Auto-translation endpoints
|
|
autoTranslate: async (translationId: number, targetLangs?: string[]): Promise<{
|
|
id: number;
|
|
source_text: string;
|
|
translations: Record<string, string>;
|
|
message: string;
|
|
}> => {
|
|
const { data } = await api.post(`/translations/auto-translate/${translationId}`, {
|
|
target_langs: targetLangs || ['en', 'mn', 'ru']
|
|
});
|
|
return data;
|
|
},
|
|
|
|
autoTranslateBatch: async (targetLangs?: string[], category?: string, overwriteExisting?: boolean): Promise<{
|
|
total_processed: number;
|
|
successful: number;
|
|
failed: number;
|
|
results: Array<{ id: number; source_text: string; success: boolean; error?: string }>;
|
|
}> => {
|
|
const { data } = await api.post('/translations/auto-translate-batch', {
|
|
target_langs: targetLangs || ['en', 'mn', 'ru'],
|
|
category,
|
|
overwrite_existing: overwriteExisting || false
|
|
});
|
|
return data;
|
|
},
|
|
|
|
translateOnDemand: async (text: string, sourceLang: string, targetLang: string): Promise<{
|
|
source_text: string;
|
|
translated_text: string;
|
|
source_lang: string;
|
|
target_lang: string;
|
|
}> => {
|
|
const { data } = await api.post('/translations/translate-on-demand', {
|
|
text,
|
|
source_lang: sourceLang,
|
|
target_lang: targetLang
|
|
});
|
|
return data;
|
|
},
|
|
|
|
getStats: async (): Promise<{
|
|
total_entries: number;
|
|
by_category: Record<string, number>;
|
|
translation_coverage: {
|
|
english: { translated: number; total: number; percentage: number };
|
|
mongolian: { translated: number; total: number; percentage: number };
|
|
russian: { translated: number; total: number; percentage: number };
|
|
};
|
|
}> => {
|
|
const { data } = await api.get('/translations/stats');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Carmodoo API
|
|
export interface CarmodooMaker {
|
|
code: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface CarmodooModel {
|
|
code: string;
|
|
name: string;
|
|
type?: string;
|
|
}
|
|
|
|
export interface CarmodooSearchResult {
|
|
id: string;
|
|
car_name: string;
|
|
maker_name?: string;
|
|
model_name?: string;
|
|
year?: number;
|
|
mileage?: number;
|
|
original_price?: number;
|
|
korea_margin?: number;
|
|
mongolia_margin?: number;
|
|
final_price?: number;
|
|
fuel?: string;
|
|
transmission?: string;
|
|
color?: string;
|
|
displacement?: number;
|
|
main_image?: string;
|
|
image_count: number;
|
|
check_num?: string; // 성능점검번호
|
|
car_key?: string; // 암호화된 차량 키 (딜러 설명 조회용)
|
|
}
|
|
|
|
export interface VehicleRequestSearchResponse {
|
|
total: number;
|
|
cars: CarmodooSearchResult[];
|
|
}
|
|
|
|
export const carmodooApi = {
|
|
getMakers: async (): Promise<CarmodooMaker[]> => {
|
|
const { data } = await api.get('/carmodoo/makers');
|
|
return data;
|
|
},
|
|
|
|
getModels: async (makerCode: string): Promise<CarmodooModel[]> => {
|
|
const { data } = await api.get(`/carmodoo/models/${makerCode}`);
|
|
return data;
|
|
},
|
|
|
|
requestSearch: async (params: {
|
|
maker_code?: string;
|
|
model_code?: string;
|
|
grade?: string;
|
|
year_min?: number;
|
|
year_max?: number;
|
|
mileage_min?: number;
|
|
mileage_max?: number;
|
|
price_min?: number;
|
|
price_max?: number;
|
|
fuel?: string;
|
|
transmission?: string;
|
|
displacement_min?: number;
|
|
displacement_max?: number;
|
|
page?: number;
|
|
page_size?: number;
|
|
}): Promise<VehicleRequestSearchResponse> => {
|
|
const { data } = await api.get('/carmodoo/request-search', { params });
|
|
return data;
|
|
},
|
|
|
|
getGrades: async (makerCode: string, modelCode: string): Promise<{ code: string; name: string }[]> => {
|
|
const { data } = await api.get(`/carmodoo/grades/${makerCode}/${modelCode}`);
|
|
return data;
|
|
},
|
|
|
|
// 차량을 로컬 DB에 저장 (이미지 다운로드 포함)
|
|
importCars: async (cars: {
|
|
car_no: string;
|
|
car_name: string;
|
|
maker_name?: string;
|
|
model_name?: string;
|
|
year?: number;
|
|
mileage?: number;
|
|
price?: number;
|
|
fuel?: string;
|
|
transmission?: string;
|
|
color?: string;
|
|
displacement?: number;
|
|
main_image?: string;
|
|
check_num?: string; // 성능점검번호
|
|
car_key?: string; // 암호화된 차량 키 (딜러 설명 조회용)
|
|
}[]): Promise<{
|
|
imported: {
|
|
car_no: string;
|
|
car_id: number;
|
|
car_name: string;
|
|
images_downloaded: number;
|
|
pdf_status?: {
|
|
success: boolean;
|
|
attempts: number;
|
|
error?: string;
|
|
path?: string;
|
|
check_num?: string;
|
|
};
|
|
}[];
|
|
skipped: { car_no: string; car_id: number; reason: string }[];
|
|
errors: { car_no: string; error: string }[];
|
|
summary: {
|
|
imported_count: number;
|
|
skipped_count: number;
|
|
error_count: number;
|
|
pdf_success_count: number;
|
|
pdf_failed_count: number;
|
|
};
|
|
}> => {
|
|
const { data } = await api.post('/carmodoo/import', { cars });
|
|
return data;
|
|
},
|
|
|
|
// Translation Management (Admin)
|
|
getCarTranslations: async (carId: number): Promise<{
|
|
car_id: number;
|
|
car_name: string;
|
|
dealer_description: string | null;
|
|
translations: {
|
|
en: string | null;
|
|
mn: string | null;
|
|
ru: string | null;
|
|
};
|
|
has_translations: boolean;
|
|
papago_configured: boolean;
|
|
}> => {
|
|
const { data } = await api.get(`/carmodoo/car/${carId}/translations`);
|
|
return data;
|
|
},
|
|
|
|
updateCarTranslations: async (carId: number, translations: {
|
|
dealer_description?: string; // 한국어 원문
|
|
dealer_description_en?: string;
|
|
dealer_description_mn?: string;
|
|
dealer_description_ru?: string;
|
|
}): Promise<{
|
|
message: string;
|
|
car_id: number;
|
|
dealer_description?: string;
|
|
translations: { en: string | null; mn: string | null; ru: string | null };
|
|
}> => {
|
|
const { data } = await api.put(`/carmodoo/car/${carId}/translations`, translations);
|
|
return data;
|
|
},
|
|
|
|
regenerateTranslations: async (carId: number): Promise<{
|
|
message: string;
|
|
car_id: number;
|
|
translations: { en: string | null; mn: string | null; ru: string | null };
|
|
}> => {
|
|
const { data } = await api.post(`/carmodoo/car/${carId}/translations/regenerate`);
|
|
return data;
|
|
},
|
|
|
|
getUntranslatedCars: async (limit?: number): Promise<{
|
|
count: number;
|
|
cars: {
|
|
id: number;
|
|
car_name: string;
|
|
dealer_description: string;
|
|
has_en: boolean;
|
|
has_mn: boolean;
|
|
has_ru: boolean;
|
|
}[];
|
|
}> => {
|
|
const { data } = await api.get('/carmodoo/admin/untranslated-cars', { params: { limit } });
|
|
return data;
|
|
},
|
|
|
|
translateAllPending: async (): Promise<{
|
|
message: string;
|
|
total: number;
|
|
success: number;
|
|
failed: number;
|
|
results: { car_id: number; status: string; error?: string }[];
|
|
}> => {
|
|
const { data } = await api.post('/carmodoo/admin/translate-all-pending');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Settings API
|
|
export interface SystemSettings {
|
|
id: number;
|
|
search_page_size: number;
|
|
korea_margin_percent: number;
|
|
mongolia_margin_percent: number;
|
|
cc_per_usdc: number;
|
|
cc_per_view: number;
|
|
cc_signup_bonus: number;
|
|
cache_ttl_hours: number;
|
|
board_enabled?: boolean;
|
|
}
|
|
|
|
export const settingsApi = {
|
|
getSettings: async (): Promise<SystemSettings> => {
|
|
const { data } = await api.get('/settings/');
|
|
return data;
|
|
},
|
|
|
|
getSearchPageSize: async (): Promise<{ search_page_size: number }> => {
|
|
const { data } = await api.get('/settings/search-page-size');
|
|
return data;
|
|
},
|
|
|
|
updateSettings: async (settings: Partial<SystemSettings>): Promise<SystemSettings> => {
|
|
const { data } = await api.put('/settings/', settings);
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Vehicle Request API
|
|
export interface VehicleRequest {
|
|
id: number;
|
|
user_id: number;
|
|
user_email?: string; // 관리자용
|
|
user_name?: string; // 관리자용
|
|
maker_code?: string;
|
|
maker_name?: string;
|
|
model_code?: string;
|
|
model_name?: string;
|
|
grade_code?: string;
|
|
grade_name?: string;
|
|
year_from?: number;
|
|
year_to?: number;
|
|
mileage_min?: number;
|
|
mileage_max?: number;
|
|
fuel?: string;
|
|
displacement_min?: number;
|
|
displacement_max?: number;
|
|
status: string;
|
|
admin_reviewed_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface RequestVehicle {
|
|
id: number;
|
|
request_id: number;
|
|
car_id?: number; // Reference to cars table
|
|
car_data: Record<string, any>;
|
|
is_approved: boolean;
|
|
approved_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface PurchasedVehicle {
|
|
id: number;
|
|
user_id: number;
|
|
car_name?: string;
|
|
car_data?: Record<string, any>;
|
|
car_image?: string;
|
|
vehicle_price_krw?: number;
|
|
domestic_cost_krw?: number;
|
|
shipping_cost_usd?: number;
|
|
total_cost_krw?: number;
|
|
car_type?: string;
|
|
shipping_status: number;
|
|
status_updated_at?: string;
|
|
current_location?: string;
|
|
estimated_arrival?: string;
|
|
purchased_at: string;
|
|
delivered_at?: string;
|
|
}
|
|
|
|
export interface VehicleRequestWithVehicles {
|
|
request: VehicleRequest;
|
|
approved_vehicles: RequestVehicle[];
|
|
}
|
|
|
|
// Directly purchased car (from banner with 1CC)
|
|
export interface DirectPurchasedCar {
|
|
id: number;
|
|
car_id: number;
|
|
car_data: Record<string, any>;
|
|
cc_paid: number;
|
|
purchased_at: string;
|
|
}
|
|
|
|
// Full response including both recommended and directly purchased cars
|
|
export interface MyVehiclesResponse {
|
|
vehicle_requests: VehicleRequestWithVehicles[];
|
|
direct_purchases: DirectPurchasedCar[];
|
|
}
|
|
|
|
export const vehicleRequestsApi = {
|
|
// User endpoints
|
|
createRequest: async (requestData: {
|
|
maker_code: string;
|
|
maker_name?: string;
|
|
model_code: string;
|
|
model_name?: string;
|
|
grade_code?: string;
|
|
grade_name?: string;
|
|
year_from?: number;
|
|
year_to?: number;
|
|
mileage_min?: number;
|
|
mileage_max?: number;
|
|
fuel?: string;
|
|
displacement_min?: number;
|
|
displacement_max?: number;
|
|
}): Promise<VehicleRequest> => {
|
|
const { data } = await api.post('/vehicle-requests/', requestData);
|
|
return data;
|
|
},
|
|
|
|
getMyRequests: async (): Promise<VehicleRequestWithVehicles[]> => {
|
|
const { data } = await api.get('/vehicle-requests/my-requests');
|
|
return data;
|
|
},
|
|
|
|
// Get all vehicles: recommended + directly purchased from banners
|
|
getMyVehicles: async (): Promise<MyVehiclesResponse> => {
|
|
const { data } = await api.get('/vehicle-requests/my-vehicles');
|
|
return data;
|
|
},
|
|
|
|
getPurchasedVehicles: async (): Promise<PurchasedVehicle[]> => {
|
|
const { data } = await api.get('/vehicle-requests/purchased');
|
|
return data;
|
|
},
|
|
|
|
// Admin endpoints
|
|
adminGetAllRequests: async (status?: string): Promise<VehicleRequest[]> => {
|
|
const params = status ? { status } : {};
|
|
const { data } = await api.get('/vehicle-requests/admin/list', { params });
|
|
return data;
|
|
},
|
|
|
|
adminGetRequestDetail: async (requestId: number): Promise<VehicleRequestWithVehicles> => {
|
|
const { data } = await api.get(`/vehicle-requests/admin/${requestId}`);
|
|
return data;
|
|
},
|
|
|
|
adminAddVehicle: async (requestId: number, vehicleData: {
|
|
request_id: number;
|
|
car_data: Record<string, any>;
|
|
is_approved?: boolean;
|
|
}): Promise<RequestVehicle> => {
|
|
const { data } = await api.post(`/vehicle-requests/admin/${requestId}/vehicles`, vehicleData);
|
|
return data;
|
|
},
|
|
|
|
adminApproveVehicles: async (requestId: number, vehicleIds: number[]): Promise<{ message: string }> => {
|
|
const { data } = await api.post(`/vehicle-requests/admin/${requestId}/approve-vehicles`, { vehicle_ids: vehicleIds });
|
|
return data;
|
|
},
|
|
|
|
adminUpdateRequestStatus: async (requestId: number, newStatus: string): Promise<{ message: string }> => {
|
|
const { data } = await api.put(`/vehicle-requests/admin/${requestId}/status`, null, { params: { new_status: newStatus } });
|
|
return data;
|
|
},
|
|
|
|
adminDeleteVehicle: async (requestId: number, vehicleId: number): Promise<{ message: string }> => {
|
|
const { data } = await api.delete(`/vehicle-requests/admin/${requestId}/vehicles/${vehicleId}`);
|
|
return data;
|
|
},
|
|
|
|
adminCreatePurchased: async (userId: number, vehicleData: {
|
|
car_name: string;
|
|
car_data?: Record<string, any>;
|
|
car_image?: string;
|
|
vehicle_price_krw: number;
|
|
domestic_cost_krw: number;
|
|
shipping_cost_usd: number;
|
|
total_cost_krw: number;
|
|
car_type: string;
|
|
}): Promise<PurchasedVehicle> => {
|
|
const { data } = await api.post('/vehicle-requests/admin/purchased', vehicleData, { params: { user_id: userId } });
|
|
return data;
|
|
},
|
|
|
|
adminGetAllPurchased: async (): Promise<PurchasedVehicle[]> => {
|
|
const { data } = await api.get('/vehicle-requests/admin/purchased/all');
|
|
return data;
|
|
},
|
|
|
|
adminUpdateShippingStatus: async (vehicleId: number, statusUpdate: {
|
|
shipping_status: number;
|
|
current_location?: string;
|
|
estimated_arrival?: string;
|
|
}): Promise<PurchasedVehicle> => {
|
|
const { data } = await api.put(`/vehicle-requests/admin/purchased/${vehicleId}/status`, statusUpdate);
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// CC (Coin) API
|
|
export interface PaymentInfo {
|
|
usdc_wallet_address: string;
|
|
usdc_network: string;
|
|
min_charge_usd: number;
|
|
max_charge_usd: number;
|
|
supported_currencies: string[];
|
|
supported_methods: string[];
|
|
rate: string;
|
|
}
|
|
|
|
export interface ChargeHistory {
|
|
id: number;
|
|
amount: number;
|
|
amount_usd?: number;
|
|
currency: string;
|
|
cc_amount: number;
|
|
payment_method: string;
|
|
transaction_id?: string;
|
|
status: string;
|
|
created_at?: string;
|
|
}
|
|
|
|
export interface AdminPayment extends ChargeHistory {
|
|
user_id: number;
|
|
user_email?: string;
|
|
user_name?: string;
|
|
wallet_address?: string;
|
|
admin_note?: string;
|
|
verified_at?: string;
|
|
}
|
|
|
|
export interface AdminPaymentListResponse {
|
|
payments: AdminPayment[];
|
|
total: number;
|
|
page: number;
|
|
page_size: number;
|
|
}
|
|
|
|
export const ccApi = {
|
|
getBalance: async (): Promise<{ cc_balance: number }> => {
|
|
const { data } = await api.get('/cc/balance');
|
|
return data;
|
|
},
|
|
|
|
getPurchasedViews: async (): Promise<CarView[]> => {
|
|
const { data } = await api.get('/cc/views');
|
|
return data;
|
|
},
|
|
|
|
getPurchasedCarIds: async (): Promise<{ car_ids: number[] }> => {
|
|
const { data } = await api.get('/cc/views/car-ids');
|
|
return data;
|
|
},
|
|
|
|
purchaseView: async (carId: number): Promise<{ message: string; cc_balance: number; car_id: number }> => {
|
|
const { data } = await api.post('/cc/purchase-view', { car_id: carId });
|
|
return data;
|
|
},
|
|
|
|
checkView: async (carId: number): Promise<{ has_access: boolean; cc_balance: number }> => {
|
|
const { data } = await api.get(`/cc/check-view/${carId}`);
|
|
return data;
|
|
},
|
|
|
|
getPaymentInfo: async (): Promise<PaymentInfo> => {
|
|
const { data } = await api.get('/cc/payment-info');
|
|
return data;
|
|
},
|
|
|
|
chargeCC: async (chargeData: {
|
|
amount: number;
|
|
currency?: string;
|
|
payment_method?: string;
|
|
transaction_id?: string;
|
|
wallet_address?: string;
|
|
}): Promise<{
|
|
message: string;
|
|
charge_id: number;
|
|
amount: number;
|
|
currency: string;
|
|
cc_amount: number;
|
|
status: string;
|
|
payment_info?: { usdc_wallet?: string; network?: string };
|
|
}> => {
|
|
const { data } = await api.post('/cc/charge', chargeData);
|
|
return data;
|
|
},
|
|
|
|
chargeUSDC: async (chargeData: {
|
|
amount_usdc: number;
|
|
transaction_hash: string;
|
|
wallet_address: string;
|
|
network?: string;
|
|
}): Promise<{
|
|
message: string;
|
|
charge_id: number;
|
|
amount_usdc: number;
|
|
cc_amount: number;
|
|
status: string;
|
|
transaction_hash: string;
|
|
}> => {
|
|
const { data } = await api.post('/cc/charge/usdc', chargeData);
|
|
return data;
|
|
},
|
|
|
|
getChargeHistory: async (): Promise<ChargeHistory[]> => {
|
|
const { data } = await api.get('/cc/charge-history');
|
|
return data;
|
|
},
|
|
|
|
// Admin endpoints
|
|
adminGetPendingPayments: async (): Promise<AdminPayment[]> => {
|
|
const { data } = await api.get('/cc/admin/pending');
|
|
return data;
|
|
},
|
|
|
|
adminGetAllPayments: async (params: {
|
|
status?: string;
|
|
page?: number;
|
|
page_size?: number;
|
|
}): Promise<AdminPaymentListResponse> => {
|
|
const { data } = await api.get('/cc/admin/all', { params });
|
|
return data;
|
|
},
|
|
|
|
adminVerifyPayment: async (chargeId: number, approved: boolean, adminNote?: string): Promise<{
|
|
message: string;
|
|
charge_id: number;
|
|
new_status: string;
|
|
}> => {
|
|
const { data } = await api.put(`/cc/admin/${chargeId}/verify`, null, {
|
|
params: { approved, admin_note: adminNote }
|
|
});
|
|
return data;
|
|
},
|
|
|
|
// Performance Check APIs (0.1 CC)
|
|
checkPerformanceCheckAccess: async (carId: number): Promise<{
|
|
has_access: boolean;
|
|
has_performance_check: boolean;
|
|
cc_balance: number;
|
|
cost: number;
|
|
}> => {
|
|
const { data } = await api.get(`/cc/check-performance-check/${carId}`);
|
|
return data;
|
|
},
|
|
|
|
purchasePerformanceCheck: async (carId: number): Promise<{
|
|
message: string;
|
|
cc_balance: number;
|
|
car_id: number;
|
|
}> => {
|
|
const { data } = await api.post('/cc/purchase-performance-check', { car_id: carId });
|
|
return data;
|
|
},
|
|
|
|
// Get performance check data from carmodoo API
|
|
getPerformanceCheck: async (carId: number): Promise<{
|
|
car_id: number;
|
|
found: boolean;
|
|
has_access: boolean;
|
|
preview?: {
|
|
check_number: string;
|
|
check_date: string | null;
|
|
mileage: number | null;
|
|
};
|
|
data: any | null;
|
|
}> => {
|
|
const { data } = await api.get(`/carmodoo/car/${carId}/performance-check`);
|
|
return data;
|
|
},
|
|
|
|
// Download performance check PDF
|
|
downloadPerformanceCheckPdf: async (carId: number): Promise<Blob> => {
|
|
const response = await api.get(`/carmodoo/car/${carId}/performance-check/pdf`, {
|
|
responseType: 'blob'
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
// Get PDF URL for iframe/embed viewing
|
|
getPerformanceCheckPdfUrl: (carId: number): string => {
|
|
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
|
|
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
|
return `${baseUrl}/api/carmodoo/car/${carId}/performance-check/pdf`;
|
|
},
|
|
};
|
|
|
|
// Withdrawal API
|
|
export interface WithdrawalBalance {
|
|
total_earned: number;
|
|
total_withdrawn: number;
|
|
pending_withdrawal: number;
|
|
available_balance: number;
|
|
}
|
|
|
|
export interface WithdrawalRequest {
|
|
id: number;
|
|
user_id: number;
|
|
amount: number;
|
|
tax_withheld: number;
|
|
net_amount: number;
|
|
bank_name: string;
|
|
bank_account: string;
|
|
account_holder: string;
|
|
status: string;
|
|
admin_note?: string;
|
|
requested_at: string;
|
|
processed_at?: string;
|
|
}
|
|
|
|
export const withdrawalApi = {
|
|
getBalance: async (): Promise<WithdrawalBalance> => {
|
|
const { data } = await api.get('/withdrawal/balance');
|
|
return data;
|
|
},
|
|
|
|
createRequest: async (requestData: {
|
|
amount: number;
|
|
bank_name: string;
|
|
bank_account: string;
|
|
account_holder: string;
|
|
}): Promise<WithdrawalRequest> => {
|
|
const { data } = await api.post('/withdrawal/request', requestData);
|
|
return data;
|
|
},
|
|
|
|
getMyRequests: async (): Promise<WithdrawalRequest[]> => {
|
|
const { data } = await api.get('/withdrawal/my-requests');
|
|
return data;
|
|
},
|
|
|
|
// Admin endpoints
|
|
adminGetAllRequests: async (status?: string): Promise<WithdrawalRequest[]> => {
|
|
const params = status ? { status_filter: status } : {};
|
|
const { data } = await api.get('/withdrawal/admin/list', { params });
|
|
return data;
|
|
},
|
|
|
|
adminProcessRequest: async (requestId: number, processData: {
|
|
status: string;
|
|
admin_note?: string;
|
|
}): Promise<WithdrawalRequest> => {
|
|
const { data } = await api.put(`/withdrawal/admin/${requestId}/process`, processData);
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Referral API
|
|
export interface ReferralReward {
|
|
id: number;
|
|
referrer_id: number;
|
|
referred_user_id: number;
|
|
payment_amount: number;
|
|
reward_amount: number;
|
|
status: string;
|
|
created_at: string;
|
|
credited_at?: string;
|
|
}
|
|
|
|
export interface ReferralStats {
|
|
total_referrals: number;
|
|
total_rewards_earned: number;
|
|
total_rewards_credited: number;
|
|
total_rewards_pending: number;
|
|
available_for_withdrawal: number;
|
|
}
|
|
|
|
export interface ReferralSettings {
|
|
referral_reward_enabled: boolean;
|
|
referral_reward_percent: number;
|
|
referral_reward_type: string;
|
|
}
|
|
|
|
export const referralApi = {
|
|
getMyLink: async (): Promise<{ referral_code: string; referral_link: string }> => {
|
|
const { data } = await api.get('/referral/my-link');
|
|
return data;
|
|
},
|
|
|
|
getMyRewards: async (): Promise<ReferralReward[]> => {
|
|
const { data } = await api.get('/referral/my-rewards');
|
|
return data;
|
|
},
|
|
|
|
getStats: async (): Promise<ReferralStats> => {
|
|
const { data } = await api.get('/referral/stats');
|
|
return data;
|
|
},
|
|
|
|
getSettings: async (): Promise<ReferralSettings> => {
|
|
const { data } = await api.get('/referral/settings');
|
|
return data;
|
|
},
|
|
|
|
// Admin endpoints
|
|
adminUpdateSettings: async (settings: Partial<ReferralSettings>): Promise<ReferralSettings> => {
|
|
const { data } = await api.put('/referral/admin/settings', settings);
|
|
return data;
|
|
},
|
|
|
|
adminGetAllRewards: async (): Promise<ReferralReward[]> => {
|
|
const { data } = await api.get('/referral/admin/all-rewards');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Notification API
|
|
export interface Notification {
|
|
id: number;
|
|
user_id: number;
|
|
notification_type: string;
|
|
title: string;
|
|
message: string;
|
|
link?: string;
|
|
related_id?: number;
|
|
related_type?: string;
|
|
is_read: boolean;
|
|
read_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface NotificationListResponse {
|
|
notifications: Notification[];
|
|
unread_count: number;
|
|
total: number;
|
|
}
|
|
|
|
export const notificationApi = {
|
|
getNotifications: async (page: number = 1, pageSize: number = 20, unreadOnly: boolean = false): Promise<NotificationListResponse> => {
|
|
const { data } = await api.get('/notifications/', {
|
|
params: { page, page_size: pageSize, unread_only: unreadOnly }
|
|
});
|
|
return data;
|
|
},
|
|
|
|
getUnreadCount: async (): Promise<{ unread_count: number }> => {
|
|
const { data } = await api.get('/notifications/unread-count');
|
|
return data;
|
|
},
|
|
|
|
markAsRead: async (notificationIds: number[]): Promise<{ message: string }> => {
|
|
const { data } = await api.post('/notifications/mark-read', { notification_ids: notificationIds });
|
|
return data;
|
|
},
|
|
|
|
markAllAsRead: async (): Promise<{ message: string }> => {
|
|
const { data } = await api.post('/notifications/mark-all-read');
|
|
return data;
|
|
},
|
|
|
|
deleteNotification: async (notificationId: number): Promise<{ message: string }> => {
|
|
const { data } = await api.delete(`/notifications/${notificationId}`);
|
|
return data;
|
|
},
|
|
|
|
// Admin endpoints
|
|
adminSendNotification: async (notificationData: {
|
|
user_id: number;
|
|
notification_type: string;
|
|
title: string;
|
|
message: string;
|
|
link?: string;
|
|
}): Promise<Notification> => {
|
|
const { data } = await api.post('/notifications/admin/send', notificationData);
|
|
return data;
|
|
},
|
|
|
|
adminSendToAll: async (title: string, message: string, link?: string): Promise<{ message: string }> => {
|
|
const { data } = await api.post('/notifications/admin/send-all', null, {
|
|
params: { title, message, link }
|
|
});
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Inquiry API
|
|
export interface Inquiry {
|
|
id: number;
|
|
user_id: number;
|
|
car_id?: number;
|
|
category: string;
|
|
subject?: string;
|
|
message: string;
|
|
contact_email?: string;
|
|
contact_phone?: string;
|
|
status: string;
|
|
admin_response?: string;
|
|
responded_at?: string;
|
|
responded_by?: number;
|
|
created_at: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
export interface InquiryMessage {
|
|
id: number;
|
|
inquiry_id: number;
|
|
user_id: number;
|
|
message: string;
|
|
is_admin: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface InquiryListResponse {
|
|
inquiries: Inquiry[];
|
|
total: number;
|
|
total_pages?: number;
|
|
page?: number;
|
|
page_size?: number;
|
|
}
|
|
|
|
export interface InquiryWithMessages {
|
|
inquiry: Inquiry;
|
|
messages: InquiryMessage[];
|
|
}
|
|
|
|
export interface InquiryStats {
|
|
total: number;
|
|
pending: number;
|
|
in_progress: number;
|
|
resolved: number;
|
|
closed: number;
|
|
}
|
|
|
|
// User Management API (Admin)
|
|
export interface AdminUser {
|
|
id: number;
|
|
email: string;
|
|
name?: string;
|
|
phone?: string;
|
|
country?: string;
|
|
cc_balance: number;
|
|
is_dealer: boolean;
|
|
referral_code?: string;
|
|
referred_by?: string;
|
|
created_at?: string;
|
|
}
|
|
|
|
export interface AdminUserListResponse {
|
|
users: AdminUser[];
|
|
total: number;
|
|
page: number;
|
|
page_size: number;
|
|
}
|
|
|
|
export const adminUserApi = {
|
|
getUsers: async (params: {
|
|
page?: number;
|
|
page_size?: number;
|
|
search?: string;
|
|
is_dealer?: boolean;
|
|
}): Promise<AdminUserListResponse> => {
|
|
const { data } = await api.get('/auth/admin/users', { params });
|
|
return data;
|
|
},
|
|
|
|
getUser: async (userId: number): Promise<AdminUser> => {
|
|
const { data } = await api.get(`/auth/admin/users/${userId}`);
|
|
return data;
|
|
},
|
|
|
|
updateUser: async (userId: number, userData: {
|
|
name?: string;
|
|
phone?: string;
|
|
country?: string;
|
|
}): Promise<AdminUser> => {
|
|
const { data } = await api.put(`/auth/admin/users/${userId}`, userData);
|
|
return data;
|
|
},
|
|
|
|
adjustCC: async (userId: number, amount: number, reason?: string): Promise<{ message: string; new_balance: number }> => {
|
|
const { data } = await api.put(`/auth/admin/users/${userId}/cc`, null, {
|
|
params: { amount, reason }
|
|
});
|
|
return data;
|
|
},
|
|
|
|
// 사용자 삭제 (관리자)
|
|
deleteUser: async (userId: number, hardDelete: boolean = false): Promise<{
|
|
message: string;
|
|
deleted_user_id: number;
|
|
hard_delete: boolean;
|
|
deleted_at?: string;
|
|
}> => {
|
|
const { data } = await api.delete(`/auth/admin/users/${userId}`, {
|
|
params: { hard_delete: hardDelete }
|
|
});
|
|
return data;
|
|
},
|
|
|
|
// 탈퇴 요청 사용자 목록
|
|
getWithdrawnUsers: async (page: number = 1, pageSize: number = 20): Promise<AdminUserListResponse & {
|
|
users: (AdminUser & { withdrawal_requested_at?: string; withdrawal_reason?: string })[];
|
|
}> => {
|
|
const { data } = await api.get('/auth/admin/users/withdrawn', {
|
|
params: { page, page_size: pageSize }
|
|
});
|
|
return data;
|
|
},
|
|
|
|
// 삭제된 사용자 목록
|
|
getDeletedUsers: async (params: {
|
|
page?: number;
|
|
page_size?: number;
|
|
search?: string;
|
|
}): Promise<AdminUserListResponse & {
|
|
users: (AdminUser & { deleted_at?: string; withdrawal_reason?: string })[];
|
|
}> => {
|
|
const { data } = await api.get('/auth/admin/users/deleted', { params });
|
|
return data;
|
|
},
|
|
|
|
// 삭제된 사용자 복원
|
|
restoreUser: async (userId: number): Promise<{ message: string; user_id: number }> => {
|
|
const { data } = await api.post(`/auth/admin/users/${userId}/restore`);
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Admin PDF API (성능점검표 PDF 관리)
|
|
export interface PdfFailure {
|
|
car_id: number;
|
|
check_num: string;
|
|
error: string;
|
|
timestamp: string;
|
|
retried: boolean;
|
|
}
|
|
|
|
export interface PdfRetryResult {
|
|
car_id: number;
|
|
check_number: string;
|
|
status: 'success' | 'failed' | 'error';
|
|
pdf_path?: string;
|
|
error?: string;
|
|
}
|
|
|
|
export const adminPdfApi = {
|
|
// PDF 생성 실패 목록 조회
|
|
getFailures: async (): Promise<{ failures: PdfFailure[]; count: number }> => {
|
|
const { data } = await api.get('/carmodoo/admin/pdf-failures');
|
|
return data;
|
|
},
|
|
|
|
// 모든 실패한 PDF 재시도
|
|
retryAllFailed: async (): Promise<{
|
|
total: number;
|
|
success: number;
|
|
failed: number;
|
|
results: PdfRetryResult[];
|
|
}> => {
|
|
const { data } = await api.post('/carmodoo/admin/retry-all-failed-pdfs');
|
|
return data;
|
|
},
|
|
|
|
// 단일 차량 PDF 재생성
|
|
regenerateSingle: async (carId: number): Promise<{ message: string; pdf_path?: string }> => {
|
|
const { data } = await api.post(`/carmodoo/car/${carId}/regenerate-pdf`);
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Dashboard API
|
|
export interface DashboardStats {
|
|
total_users: number;
|
|
new_users_today: number;
|
|
new_users_this_week: number;
|
|
total_dealers: number;
|
|
pending_dealer_applications: number;
|
|
total_cars: number;
|
|
total_vehicle_requests: number;
|
|
pending_requests: number;
|
|
total_purchased_vehicles: number;
|
|
total_inquiries: number;
|
|
pending_inquiries: number;
|
|
total_shares: number;
|
|
purchased_shares: number;
|
|
total_withdrawals: number;
|
|
pending_withdrawals: number;
|
|
total_cc_charged: number;
|
|
total_withdrawal_amount: number;
|
|
}
|
|
|
|
export interface RevenueStats {
|
|
total_revenue: number;
|
|
revenue_this_month: number;
|
|
revenue_last_month: number;
|
|
platform_commission: number;
|
|
dealer_commission: number;
|
|
}
|
|
|
|
export interface ChartData {
|
|
labels: string[];
|
|
values: number[];
|
|
}
|
|
|
|
export interface RecentActivity {
|
|
type: string;
|
|
title: string;
|
|
description: string;
|
|
time: string;
|
|
icon: string;
|
|
}
|
|
|
|
export interface TopDealer {
|
|
id: number;
|
|
name: string;
|
|
dealer_code: string;
|
|
total_sales: number;
|
|
total_commission: number;
|
|
}
|
|
|
|
export interface PendingActions {
|
|
pending_requests: number;
|
|
pending_inquiries: number;
|
|
pending_dealer_applications: number;
|
|
pending_withdrawals: number;
|
|
total_pending: number;
|
|
}
|
|
|
|
export const dashboardApi = {
|
|
getStats: async (): Promise<DashboardStats> => {
|
|
const { data } = await api.get('/dashboard/stats');
|
|
return data;
|
|
},
|
|
|
|
getRevenue: async (): Promise<RevenueStats> => {
|
|
const { data } = await api.get('/dashboard/revenue');
|
|
return data;
|
|
},
|
|
|
|
getUserChart: async (days: number = 30): Promise<ChartData> => {
|
|
const { data } = await api.get('/dashboard/chart/users', { params: { days } });
|
|
return data;
|
|
},
|
|
|
|
getRequestChart: async (days: number = 30): Promise<ChartData> => {
|
|
const { data } = await api.get('/dashboard/chart/requests', { params: { days } });
|
|
return data;
|
|
},
|
|
|
|
getRevenueChart: async (days: number = 30): Promise<ChartData> => {
|
|
const { data } = await api.get('/dashboard/chart/revenue', { params: { days } });
|
|
return data;
|
|
},
|
|
|
|
getRecentActivities: async (limit: number = 10): Promise<RecentActivity[]> => {
|
|
const { data } = await api.get('/dashboard/recent-activities', { params: { limit } });
|
|
return data;
|
|
},
|
|
|
|
getTopDealers: async (limit: number = 5): Promise<TopDealer[]> => {
|
|
const { data } = await api.get('/dashboard/top-dealers', { params: { limit } });
|
|
return data;
|
|
},
|
|
|
|
getPendingActions: async (): Promise<PendingActions> => {
|
|
const { data } = await api.get('/dashboard/pending-actions');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Push Notification API
|
|
export interface PushSubscriptionData {
|
|
endpoint: string;
|
|
p256dh_key: string;
|
|
auth_key: string;
|
|
device_info?: string;
|
|
}
|
|
|
|
export interface NotificationPreferences {
|
|
vehicle_recommended: boolean;
|
|
shipping_update: boolean;
|
|
payment_confirmed: boolean;
|
|
withdrawal_processed: boolean;
|
|
dealer_status: boolean;
|
|
share_purchased: boolean;
|
|
referral_reward: boolean;
|
|
inquiry_reply: boolean;
|
|
system_announcements: boolean;
|
|
push_enabled: boolean;
|
|
email_enabled: boolean;
|
|
}
|
|
|
|
export const pushApi = {
|
|
getVapidKey: async (): Promise<{ public_key: string }> => {
|
|
const { data } = await api.get('/push/vapid-key');
|
|
return data;
|
|
},
|
|
|
|
subscribe: async (subscription: PushSubscriptionData): Promise<{ message: string }> => {
|
|
const { data } = await api.post('/push/subscribe', subscription);
|
|
return data;
|
|
},
|
|
|
|
unsubscribe: async (endpoint: string): Promise<{ message: string }> => {
|
|
const { data } = await api.delete('/push/unsubscribe', { params: { endpoint } });
|
|
return data;
|
|
},
|
|
|
|
getSubscriptions: async (): Promise<{ id: number; endpoint: string; device_info?: string; created_at?: string }[]> => {
|
|
const { data } = await api.get('/push/subscriptions');
|
|
return data;
|
|
},
|
|
|
|
getPreferences: async (): Promise<NotificationPreferences> => {
|
|
const { data } = await api.get('/push/preferences');
|
|
return data;
|
|
},
|
|
|
|
updatePreferences: async (preferences: Partial<NotificationPreferences>): Promise<{ message: string }> => {
|
|
const { data } = await api.put('/push/preferences', preferences);
|
|
return data;
|
|
},
|
|
|
|
// Admin
|
|
adminGetStats: async (): Promise<{ total_subscriptions: number; users_with_push: number }> => {
|
|
const { data } = await api.get('/push/admin/stats');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
export const inquiryApi = {
|
|
// User endpoints
|
|
getInquiries: async (): Promise<Inquiry[]> => {
|
|
const { data } = await api.get('/inquiries');
|
|
return data;
|
|
},
|
|
|
|
createInquiry: async (inquiryData: {
|
|
category?: string;
|
|
subject?: string;
|
|
message: string;
|
|
contact_email?: string;
|
|
contact_phone?: string;
|
|
car_id?: number;
|
|
}): Promise<Inquiry> => {
|
|
const { data } = await api.post('/inquiries', inquiryData);
|
|
return data;
|
|
},
|
|
|
|
getMyInquiries: async (page: number = 1, pageSize: number = 10, status?: string): Promise<InquiryListResponse> => {
|
|
const params: Record<string, any> = { page, page_size: pageSize };
|
|
if (status) params.status = status;
|
|
const { data } = await api.get('/inquiries/my-inquiries', { params });
|
|
return data;
|
|
},
|
|
|
|
getInquiryDetail: async (inquiryId: number): Promise<InquiryWithMessages> => {
|
|
const { data } = await api.get(`/inquiries/my-inquiries/${inquiryId}`);
|
|
return data;
|
|
},
|
|
|
|
addMessage: async (inquiryId: number, message: string): Promise<InquiryMessage> => {
|
|
const { data } = await api.post(`/inquiries/my-inquiries/${inquiryId}/message`, { message });
|
|
return data;
|
|
},
|
|
|
|
// Admin endpoints
|
|
adminGetInquiries: async (page: number = 1, pageSize: number = 20, status?: string, category?: string): Promise<InquiryListResponse> => {
|
|
const params: Record<string, any> = { page, page_size: pageSize };
|
|
if (status) params.status = status;
|
|
if (category) params.category = category;
|
|
const { data } = await api.get('/inquiries/admin/list', { params });
|
|
return data;
|
|
},
|
|
|
|
adminGetInquiryDetail: async (inquiryId: number): Promise<InquiryWithMessages> => {
|
|
const { data } = await api.get(`/inquiries/admin/${inquiryId}`);
|
|
return data;
|
|
},
|
|
|
|
adminRespond: async (inquiryId: number, responseData: { message: string; status?: string }): Promise<InquiryMessage> => {
|
|
const { data } = await api.post(`/inquiries/admin/${inquiryId}/respond`, responseData);
|
|
return data;
|
|
},
|
|
|
|
adminUpdateStatus: async (inquiryId: number, status: string): Promise<Inquiry> => {
|
|
const { data } = await api.put(`/inquiries/admin/${inquiryId}/status`, { status });
|
|
return data;
|
|
},
|
|
|
|
adminGetStats: async (): Promise<InquiryStats> => {
|
|
const { data } = await api.get('/inquiries/admin/stats');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Exchange Rate API
|
|
export interface ExchangeRateSimple {
|
|
[currencyCode: string]: {
|
|
rate: number; // KRW per 1 unit (e.g., 1 USD = 1450 KRW)
|
|
symbol: string;
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
export const exchangeRateApi = {
|
|
getSimpleRates: async (): Promise<ExchangeRateSimple> => {
|
|
const { data } = await api.get('/exchange-rate/simple');
|
|
return data;
|
|
},
|
|
|
|
refreshRates: async (): Promise<any> => {
|
|
const { data } = await api.post('/exchange-rate/refresh');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Verification API
|
|
export interface VerificationResponse {
|
|
success: boolean;
|
|
message: string;
|
|
}
|
|
|
|
export interface VerificationStatus {
|
|
email_verified: boolean;
|
|
phone_verified: boolean;
|
|
email?: string;
|
|
phone?: string;
|
|
}
|
|
|
|
export const verificationApi = {
|
|
// Email verification for pre-registration
|
|
sendEmailCodePreregister: async (email: string, language: string = 'en'): Promise<VerificationResponse> => {
|
|
const { data } = await api.post('/verification/email/send-preregister', { email, language });
|
|
return data;
|
|
},
|
|
|
|
verifyEmailCodePreregister: async (email: string, code: string): Promise<VerificationResponse> => {
|
|
const { data } = await api.post('/verification/email/verify-preregister', { email, code });
|
|
return data;
|
|
},
|
|
|
|
// Email verification for logged-in users
|
|
sendEmailCode: async (email: string, language: string = 'en'): Promise<VerificationResponse> => {
|
|
const { data } = await api.post('/verification/email/send', { email, language });
|
|
return data;
|
|
},
|
|
|
|
verifyEmailCode: async (code: string, email?: string): Promise<VerificationResponse> => {
|
|
const { data } = await api.post('/verification/email/verify', { code, email });
|
|
return data;
|
|
},
|
|
|
|
// Phone verification (requires login)
|
|
sendPhoneCode: async (phone: string, language: string = 'en'): Promise<VerificationResponse> => {
|
|
const { data } = await api.post('/verification/phone/send', { phone, language });
|
|
return data;
|
|
},
|
|
|
|
verifyPhoneCode: async (phone: string, code: string): Promise<VerificationResponse> => {
|
|
const { data } = await api.post('/verification/phone/verify', { phone, code });
|
|
return data;
|
|
},
|
|
|
|
// Get verification status
|
|
getStatus: async (): Promise<VerificationStatus> => {
|
|
const { data } = await api.get('/verification/status');
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Visitor Analytics API
|
|
export interface VisitorStatsOverview {
|
|
total_visits: number;
|
|
unique_visitors: number;
|
|
device_breakdown: Record<string, number>;
|
|
browser_breakdown: Record<string, number>;
|
|
country_breakdown: Record<string, number>;
|
|
}
|
|
|
|
export interface TopPage {
|
|
path: string;
|
|
views: number;
|
|
title?: string;
|
|
}
|
|
|
|
export interface TopReferrer {
|
|
domain: string;
|
|
visits: number;
|
|
}
|
|
|
|
export interface RealtimeStats {
|
|
active_visitors: number;
|
|
minutes: number;
|
|
recent_pages: { path: string; views: number }[];
|
|
}
|
|
|
|
export const visitorApi = {
|
|
getOverview: async (days: number = 30): Promise<VisitorStatsOverview> => {
|
|
const { data } = await api.get('/visitor/admin/overview', { params: { days } });
|
|
return data;
|
|
},
|
|
|
|
getVisitsChart: async (days: number = 30): Promise<ChartData> => {
|
|
const { data } = await api.get('/visitor/admin/chart/visits', { params: { days } });
|
|
return data;
|
|
},
|
|
|
|
getUniqueVisitorsChart: async (days: number = 30): Promise<ChartData> => {
|
|
const { data } = await api.get('/visitor/admin/chart/unique-visitors', { params: { days } });
|
|
return data;
|
|
},
|
|
|
|
getTopPages: async (days: number = 30, limit: number = 20): Promise<TopPage[]> => {
|
|
const { data } = await api.get('/visitor/admin/top-pages', { params: { days, limit } });
|
|
return data;
|
|
},
|
|
|
|
getTopReferrers: async (days: number = 30, limit: number = 10): Promise<TopReferrer[]> => {
|
|
const { data } = await api.get('/visitor/admin/top-referrers', { params: { days, limit } });
|
|
return data;
|
|
},
|
|
|
|
getRealtime: async (minutes: number = 5): Promise<RealtimeStats> => {
|
|
const { data } = await api.get('/visitor/admin/realtime', { params: { minutes } });
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// SNS Share API
|
|
export interface SnsShareSubmission {
|
|
id: number;
|
|
user_id: number;
|
|
car_id: number;
|
|
platform: string;
|
|
sns_url: string;
|
|
status: string;
|
|
rejected_reason?: string;
|
|
reward_cc: number;
|
|
rewarded_at?: string;
|
|
submitted_at: string;
|
|
verified_at?: string;
|
|
verified_by?: number;
|
|
car_name?: string;
|
|
car_image?: string;
|
|
}
|
|
|
|
export interface SnsShareListResponse {
|
|
submissions: SnsShareSubmission[];
|
|
total: number;
|
|
pending_count: number;
|
|
approved_count: number;
|
|
rejected_count: number;
|
|
}
|
|
|
|
export interface SnsShareStats {
|
|
total_submissions: number;
|
|
pending_submissions: number;
|
|
approved_submissions: number;
|
|
rejected_submissions: number;
|
|
total_rewarded_cc: number;
|
|
}
|
|
|
|
export interface CampaignStatus {
|
|
enabled: boolean;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
sns_share_reward_cc?: number;
|
|
referral_signup_reward_cc?: number;
|
|
message?: string;
|
|
}
|
|
|
|
export const snsShareApi = {
|
|
// Public: 캠페인 상태 조회
|
|
getCampaignStatus: async (): Promise<CampaignStatus> => {
|
|
const { data } = await api.get('/sns-share/campaign-status');
|
|
return data;
|
|
},
|
|
|
|
// User: SNS 공유 URL 제출
|
|
submit: async (carId: number, platform: string, snsUrl: string): Promise<SnsShareSubmission> => {
|
|
const { data } = await api.post('/sns-share/submit', {
|
|
car_id: carId,
|
|
platform,
|
|
sns_url: snsUrl,
|
|
});
|
|
return data;
|
|
},
|
|
|
|
// User: 내 제출 목록 조회
|
|
getMySubmissions: async (): Promise<SnsShareListResponse> => {
|
|
const { data } = await api.get('/sns-share/my-submissions');
|
|
return data;
|
|
},
|
|
|
|
// Admin: 전체 제출 목록 조회
|
|
getAdminList: async (statusFilter?: string, skip: number = 0, limit: number = 50): Promise<SnsShareListResponse> => {
|
|
const params: any = { skip, limit };
|
|
if (statusFilter) params.status_filter = statusFilter;
|
|
const { data } = await api.get('/sns-share/admin/list', { params });
|
|
return data;
|
|
},
|
|
|
|
// Admin: 통계 조회
|
|
getAdminStats: async (): Promise<SnsShareStats> => {
|
|
const { data } = await api.get('/sns-share/admin/stats');
|
|
return data;
|
|
},
|
|
|
|
// Admin: 검증 (승인/거부)
|
|
verify: async (submissionId: number, approved: boolean, rejectedReason?: string): Promise<SnsShareSubmission> => {
|
|
const { data } = await api.put(`/sns-share/admin/${submissionId}/verify`, {
|
|
approved,
|
|
rejected_reason: rejectedReason,
|
|
});
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// Board (Bulletin) API Types
|
|
export interface BoardCategory {
|
|
id: number;
|
|
name: string;
|
|
name_en?: string;
|
|
name_mn?: string;
|
|
name_ru?: string;
|
|
slug: string;
|
|
description?: string;
|
|
sort_order: number;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
updated_at?: string;
|
|
post_count: number;
|
|
}
|
|
|
|
export interface BoardPostListItem {
|
|
id: number;
|
|
title: string;
|
|
category_id: number;
|
|
category_name?: string;
|
|
author_id: number;
|
|
author_name?: string;
|
|
is_notice: boolean;
|
|
is_pinned: boolean;
|
|
view_count: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface BoardPost {
|
|
id: number;
|
|
title: string;
|
|
content: string;
|
|
category_id: number;
|
|
category?: BoardCategory;
|
|
author_id: number;
|
|
author?: {
|
|
id: number;
|
|
name?: string;
|
|
email: string;
|
|
is_admin: boolean;
|
|
};
|
|
is_notice: boolean;
|
|
is_pinned: boolean;
|
|
is_published: boolean;
|
|
view_count: number;
|
|
created_at: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
export interface BoardPostListResponse {
|
|
posts: BoardPostListItem[];
|
|
notices: BoardPostListItem[];
|
|
total: number;
|
|
page: number;
|
|
page_size: number;
|
|
total_pages: number;
|
|
}
|
|
|
|
export interface BoardCategoryListResponse {
|
|
categories: BoardCategory[];
|
|
total: number;
|
|
}
|
|
|
|
// Board API
|
|
export const boardApi = {
|
|
// Categories
|
|
getCategories: async (includeInactive: boolean = false): Promise<BoardCategoryListResponse> => {
|
|
const { data } = await api.get('/board/categories', { params: { include_inactive: includeInactive } });
|
|
return data;
|
|
},
|
|
|
|
createCategory: async (category: {
|
|
name: string;
|
|
name_en?: string;
|
|
name_mn?: string;
|
|
name_ru?: string;
|
|
slug: string;
|
|
description?: string;
|
|
sort_order?: number;
|
|
is_active?: boolean;
|
|
}): Promise<BoardCategory> => {
|
|
const { data } = await api.post('/board/categories', category);
|
|
return data;
|
|
},
|
|
|
|
updateCategory: async (categoryId: number, category: {
|
|
name?: string;
|
|
name_en?: string;
|
|
name_mn?: string;
|
|
name_ru?: string;
|
|
slug?: string;
|
|
description?: string;
|
|
sort_order?: number;
|
|
is_active?: boolean;
|
|
}): Promise<BoardCategory> => {
|
|
const { data } = await api.put(`/board/categories/${categoryId}`, category);
|
|
return data;
|
|
},
|
|
|
|
deleteCategory: async (categoryId: number): Promise<void> => {
|
|
await api.delete(`/board/categories/${categoryId}`);
|
|
},
|
|
|
|
// Posts
|
|
getPosts: async (params: {
|
|
page?: number;
|
|
page_size?: number;
|
|
category_id?: number;
|
|
search?: string;
|
|
}): Promise<BoardPostListResponse> => {
|
|
const { data } = await api.get('/board/posts', { params });
|
|
return data;
|
|
},
|
|
|
|
getPost: async (postId: number): Promise<BoardPost> => {
|
|
const { data } = await api.get(`/board/posts/${postId}`);
|
|
return data;
|
|
},
|
|
|
|
createPost: async (post: {
|
|
title: string;
|
|
content: string;
|
|
category_id: number;
|
|
is_notice?: boolean;
|
|
}): Promise<BoardPost> => {
|
|
const { data } = await api.post('/board/posts', post);
|
|
return data;
|
|
},
|
|
|
|
updatePost: async (postId: number, post: {
|
|
title?: string;
|
|
content?: string;
|
|
category_id?: number;
|
|
is_notice?: boolean;
|
|
is_pinned?: boolean;
|
|
is_published?: boolean;
|
|
}): Promise<BoardPost> => {
|
|
const { data } = await api.put(`/board/posts/${postId}`, post);
|
|
return data;
|
|
},
|
|
|
|
deletePost: async (postId: number): Promise<void> => {
|
|
await api.delete(`/board/posts/${postId}`);
|
|
},
|
|
|
|
// Admin
|
|
getAdminPosts: async (params: {
|
|
page?: number;
|
|
page_size?: number;
|
|
category_id?: number;
|
|
is_notice?: boolean;
|
|
is_published?: boolean;
|
|
search?: string;
|
|
}): Promise<BoardPostListResponse> => {
|
|
const { data } = await api.get('/board/admin/posts', { params });
|
|
return data;
|
|
},
|
|
|
|
toggleNotice: async (postId: number): Promise<{ is_notice: boolean; is_pinned: boolean }> => {
|
|
const { data } = await api.put(`/board/admin/posts/${postId}/toggle-notice`);
|
|
return data;
|
|
},
|
|
|
|
togglePin: async (postId: number): Promise<{ is_pinned: boolean }> => {
|
|
const { data } = await api.put(`/board/admin/posts/${postId}/toggle-pin`);
|
|
return data;
|
|
},
|
|
};
|
|
|
|
export default api;
|