Files
Todo-Project/frontend/static/js/api.js
hyungi 03e6fe5b91 🔧 Fix CORS and API endpoint issues
- Fix API endpoint paths (remove trailing slashes for 405 errors)
- Update API URLs to use api-todo.hyungi.net subdomain for HTTPS compatibility
- Improve CORS settings parsing in backend (handle brackets and quotes)
- Add frontend volume mount to docker-compose for real-time file updates
- Update Synology deployment config with wildcard CORS settings

Resolves:
- 405 Method Not Allowed errors
- Mixed Content security issues (HTTPS → HTTP)
- CORS preflight request failures
- Docker build requirements for every file change

Tested: API endpoints now correctly use HTTPS subdomain, eliminating security blocks
2025-09-24 18:54:07 +09:00

220 lines
6.6 KiB
JavaScript

/**
* API 통신 유틸리티
*/
// 환경에 따른 API URL 설정
const API_BASE_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:9000/api' // 로컬 개발 환경
: window.location.hostname === 'todo.hyungi.net'
? 'https://api-todo.hyungi.net/api' // API 서브도메인 (HTTPS 통일)
: window.location.hostname === '192.168.219.116'
? 'http://192.168.219.116:9000/api' // IP 직접 접근
: window.location.protocol === 'https:'
? `https://${window.location.hostname}:9000/api` // HTTPS 환경
: `http://${window.location.hostname}:9000/api`; // HTTP 환경
class ApiClient {
constructor() {
this.token = localStorage.getItem('authToken');
}
async request(endpoint, options = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
// 인증 토큰 추가
if (this.token) {
config.headers['Authorization'] = `Bearer ${this.token}`;
console.log('API 요청에 토큰 포함:', this.token.substring(0, 20) + '...');
} else {
console.warn('API 요청에 토큰이 없습니다!');
}
try {
const response = await fetch(url, config);
if (!response.ok) {
if (response.status === 401) {
// 토큰 만료 시 로그아웃
console.error('인증 실패 - 토큰 제거 후 로그인 페이지로 이동');
// 토큰만 제거하고 페이지 리로드는 하지 않음
localStorage.removeItem('authToken');
localStorage.removeItem('currentUser');
// 무한 루프 방지: 이미 index.html이 아닌 경우만 리다이렉트
if (!window.location.pathname.endsWith('index.html') && window.location.pathname !== '/') {
window.location.href = 'index.html';
}
throw new Error('인증이 만료되었습니다. 다시 로그인해주세요.');
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error('API 요청 실패:', error);
throw error;
}
}
// GET 요청
async get(endpoint) {
// 토큰 재로드 (로그인 후 토큰이 업데이트된 경우)
this.token = localStorage.getItem('authToken');
return this.request(endpoint, { method: 'GET' });
}
// POST 요청
async post(endpoint, data) {
// 토큰 재로드
this.token = localStorage.getItem('authToken');
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
// PUT 요청
async put(endpoint, data) {
// 토큰 재로드
this.token = localStorage.getItem('authToken');
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
// DELETE 요청
async delete(endpoint) {
// 토큰 재로드
this.token = localStorage.getItem('authToken');
return this.request(endpoint, { method: 'DELETE' });
}
// 파일 업로드
async uploadFile(endpoint, formData) {
// 토큰 재로드
this.token = localStorage.getItem('authToken');
return this.request(endpoint, {
method: 'POST',
headers: {
// Content-Type을 설정하지 않음 (FormData가 자동으로 설정)
},
body: formData
});
}
// 토큰 설정
setToken(token) {
this.token = token;
localStorage.setItem('authToken', token);
}
// 로그아웃
logout() {
this.token = null;
localStorage.removeItem('authToken');
localStorage.removeItem('currentUser');
window.location.reload();
}
}
// 전역 API 클라이언트 인스턴스
const api = new ApiClient();
// 인증 관련 API
const AuthAPI = {
async login(username, password) {
const response = await api.post('/auth/login', {
username,
password
});
if (response.access_token) {
api.setToken(response.access_token);
// 사용자 정보가 있으면 저장, 없으면 기본값 사용
const user = response.user || { username: 'hyungi', full_name: 'Administrator' };
localStorage.setItem('currentUser', JSON.stringify(user));
}
return response;
},
async logout() {
try {
await api.post('/auth/logout');
} catch (error) {
console.error('로그아웃 API 호출 실패:', error);
} finally {
api.logout();
}
},
async getCurrentUser() {
return api.get('/auth/me');
}
};
// Todo 관련 API
const TodoAPI = {
async getTodos(status = null, category = null) {
let url = '/todos/'; // 슬래시 추가
const params = new URLSearchParams();
if (status && status !== 'all') params.append('status', status);
if (category && category !== 'all') params.append('category', category);
if (params.toString()) {
url += '?' + params.toString();
}
return api.get(url);
},
async createTodo(todoData) {
return api.post('/todos/', todoData); // 슬래시 추가
},
async updateTodo(id, todoData) {
return api.put(`/todos/${id}`, todoData);
},
async deleteTodo(id) {
return api.delete(`/todos/${id}`);
},
async completeTodo(id) {
return api.put(`/todos/${id}`, { status: 'completed' });
},
async getTodayTodos() {
return api.get('/calendar/today');
},
async getTodoById(id) {
return api.get(`/todos/${id}`);
},
async uploadImage(imageFile) {
const formData = new FormData();
formData.append('image', imageFile);
return api.uploadFile('/todos/upload-image', formData);
}
};
// 전역으로 사용 가능하도록 export
window.api = api;
window.AuthAPI = AuthAPI;
window.TodoAPI = TodoAPI;