feat: SAPIENS Mobile App - Initial commit

React Native mobile application for SAPIENS news platform.
Consolidated all previous history into single commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2025-10-23 14:30:25 +09:00
commit 919afe56f2
1516 changed files with 64072 additions and 0 deletions

128
sapiense-ai-app/lib/api.ts Normal file
View File

@ -0,0 +1,128 @@
import { API_BASE_URL, API_TIMEOUT } from './config';
// Generic API request helper
async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT);
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
signal: controller.signal,
...options,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API Error (${response.status}): ${errorText || response.statusText}`);
}
return response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
throw new Error('Unknown error occurred');
}
}
// Outlets API
export const outletsApi = {
getAll: async (category?: string, language = 'ko') => {
const params = new URLSearchParams();
if (category) params.append('category', category);
if (language) params.append('language', language);
const response = await apiRequest<any>(`/api/v1/outlets?${params}`);
// API returns {"people": [...]} when category is specified
// Extract the array from the categorized response
if (category && response[category]) {
return response[category];
}
return response;
},
getById: (id: string, language = 'ko') => {
const params = new URLSearchParams({ language });
return apiRequest(`/api/v1/outlets/${id}?${params}`);
},
};
// Articles API
export const articlesApi = {
getAll: (language = 'ko') => {
const params = new URLSearchParams({ page_size: '50' });
return apiRequest(`/api/v1/${language}/articles?${params}`);
},
getByOutlet: (outletId: string, language = 'ko') => {
const params = new URLSearchParams({ page_size: '50' });
return apiRequest(`/api/v1/${language}/outlets/${encodeURIComponent(outletId)}/articles?${params}`);
},
getFeatured: (limit = 10, language = 'ko') => {
const params = new URLSearchParams({ page_size: limit.toString() });
return apiRequest(`/api/v1/${language}/articles?${params}`);
},
getById: (id: string, language = 'ko', useNewsId = false) => {
return apiRequest(`/api/v1/${language}/articles/${id}`);
},
};
// Search API
export const searchApi = {
search: async (
query: string,
type: 'all' | 'articles' | 'outlets' = 'all',
language = 'en',
outletId?: string
) => {
const params = new URLSearchParams({
q: query,
type,
language,
});
if (outletId) {
params.append('outletId', outletId);
}
return apiRequest(`/api/v1/search?${params}`);
},
articles: (query: string, language = 'en') => {
const params = new URLSearchParams({ q: query, language, limit: '20' });
return apiRequest(`/api/v1/search/articles?${params}`);
},
};
// Feed API for YouTube-style interface
export const feedApi = {
getFeed: (params: {
cursor?: string;
limit?: number;
filter?: 'all' | 'people' | 'topics' | 'companies';
}) => {
const searchParams = new URLSearchParams();
if (params.cursor) searchParams.set('cursor', params.cursor);
if (params.limit) searchParams.set('limit', params.limit.toString());
if (params.filter) searchParams.set('filter', params.filter);
return apiRequest(`/api/v1/feed?${searchParams.toString()}`);
},
incrementView: (articleId: string) =>
apiRequest(`/api/v1/articles/${articleId}/view`, {
method: 'POST',
}),
};

View File

@ -0,0 +1,32 @@
// API Configuration
// For development with Expo, we need to use the computer's local IP address
// You can find your IP by running: ifconfig | grep "inet " | grep -v 127.0.0.1
// TODO: Replace with your computer's IP address when testing on physical device
// For iOS Simulator: use 'localhost'
// For Android Emulator: use '10.0.2.2'
// For physical device: use your computer's local IP (e.g., '192.168.1.100')
import Constants from 'expo-constants';
import { Platform } from 'react-native';
// Get the API URL based on the platform
function getApiUrl() {
if (__DEV__) {
// Development mode
if (Platform.OS === 'android') {
// Android emulator uses 10.0.2.2 to access localhost on the host
return 'http://10.0.2.2:8050';
}
// iOS simulator and web can use localhost
// For physical device, replace 'localhost' with your computer's IP
return 'http://localhost:8050';
}
// Production mode - use your production API URL
return 'https://your-production-api.com';
}
export const API_BASE_URL = getApiUrl();
export const API_TIMEOUT = 10000; // 10 seconds

View File

@ -0,0 +1,16 @@
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: true,
retry: 1,
staleTime: 5 * 60 * 1000, // 5 minutes
},
mutations: {
retry: false,
},
},
});