Files
M-Project/frontend/static/js/api.js
Hyungi Ahn 236e1ca493 feat: 사용자 관리에 부서 정보 추가 및 편집 기능 구현
🏢 Department Management System:
- 5개 부서 지원: 생산, 품질, 구매, 설계, 영업
- 사용자 생성/수정 시 부서 선택 가능
- 부서별 사용자 분류 및 표시

📊 Database Schema Updates:
- department_type ENUM 추가 (production, quality, purchasing, design, sales)
- users 테이블에 department 컬럼 추가
- idx_users_department 인덱스 생성 (성능 최적화)
- 014_add_user_department.sql 마이그레이션 실행

🔧 Backend Enhancements:
- DepartmentType ENUM 클래스 추가 (models.py, schemas.py)
- User 모델에 department 필드 추가
- UserBase, UserUpdate 스키마에 department 필드 포함
- 기존 API 엔드포인트 자동 호환

🎨 Frontend UI Improvements:
- 사용자 추가 폼에 부서 선택 드롭다운 추가
- 사용자 목록에 부서 정보 배지 표시 (녹색 배경)
- 사용자 편집 모달 새로 구현
- 부서명 한글 변환 함수 (AuthAPI.getDepartmentLabel)

 User Management Features:
- 편집 버튼으로 사용자 정보 수정 가능
- 부서, 이름, 권한 실시간 변경
- 사용자 ID는 수정 불가 (읽기 전용)
- 모달 기반 직관적 UI

🔍 Visual Enhancements:
- 부서 정보 아이콘 (fas fa-building)
- 색상 코딩: 부서(녹색), 권한(빨강/파랑)
- 반응형 레이아웃 (flex-1, gap-3)
- 호버 효과 및 트랜지션

🚀 API Integration:
- AuthAPI.getDepartments() - 부서 목록 반환
- AuthAPI.getDepartmentLabel() - 부서명 변환
- AuthAPI.updateUser() - 부서 정보 포함 업데이트
- 기존 createUser API 확장 지원

Expected Result:
 사용자 생성 시 부서 선택 가능
 사용자 목록에 부서 정보 표시
 편집 버튼으로 부서 변경 가능
 5개 부서 분류 시스템 완성
 직관적인 사용자 관리 UI
2025-10-25 13:29:47 +09:00

344 lines
10 KiB
JavaScript

// API 기본 설정 (Cloudflare 터널 + 로컬 환경 지원)
const API_BASE_URL = (() => {
const hostname = window.location.hostname;
const protocol = window.location.protocol;
const port = window.location.port;
console.log('🔧 API URL 생성 - hostname:', hostname, 'protocol:', protocol, 'port:', port);
// 로컬 환경 (포트 있음)
if (port === '16080') {
const url = `${protocol}//${hostname}:${port}/api`;
console.log('🏠 로컬 환경 URL:', url);
return url;
}
// Cloudflare 터널 환경 (m.hyungi.net) - 강제 HTTPS
if (hostname === 'm.hyungi.net') {
const url = `https://m-api.hyungi.net/api`;
console.log('☁️ Cloudflare 환경 URL:', url);
return url;
}
// 기타 환경
const url = '/api';
console.log('🌐 기타 환경 URL:', url);
return url;
})();
// 토큰 관리
const TokenManager = {
getToken: () => localStorage.getItem('access_token'),
setToken: (token) => localStorage.setItem('access_token', token),
removeToken: () => localStorage.removeItem('access_token'),
getUser: () => {
const userStr = localStorage.getItem('current_user');
return userStr ? JSON.parse(userStr) : null;
},
setUser: (user) => localStorage.setItem('current_user', JSON.stringify(user)),
removeUser: () => localStorage.removeItem('current_user')
};
// API 요청 헬퍼
async function apiRequest(endpoint, options = {}) {
const token = TokenManager.getToken();
const defaultHeaders = {
'Content-Type': 'application/json',
};
if (token) {
defaultHeaders['Authorization'] = `Bearer ${token}`;
}
const config = {
...options,
headers: {
...defaultHeaders,
...options.headers
}
};
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, config);
if (response.status === 401) {
// 인증 실패 시 로그인 페이지로
TokenManager.removeToken();
TokenManager.removeUser();
window.location.href = '/index.html';
return;
}
if (!response.ok) {
const error = await response.json();
console.error('API Error Response:', error);
console.error('Error details:', JSON.stringify(error, null, 2));
// 422 에러의 경우 validation 에러 메시지 추출
if (response.status === 422 && error.detail && Array.isArray(error.detail)) {
const validationErrors = error.detail.map(err =>
`${err.loc ? err.loc.join('.') : 'field'}: ${err.msg}`
).join(', ');
throw new Error(`입력값 검증 오류: ${validationErrors}`);
}
throw new Error(error.detail || 'API 요청 실패');
}
return await response.json();
} catch (error) {
console.error('API 요청 에러:', error);
throw error;
}
}
// Auth API
const AuthAPI = {
login: async (username, password) => {
const formData = new URLSearchParams();
formData.append('username', username);
formData.append('password', password);
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString()
});
if (!response.ok) {
const error = await response.json();
console.error('로그인 에러:', error);
throw new Error(error.detail || '로그인 실패');
}
const data = await response.json();
TokenManager.setToken(data.access_token);
TokenManager.setUser(data.user);
return data;
} catch (error) {
console.error('로그인 요청 에러:', error);
throw error;
}
},
logout: () => {
TokenManager.removeToken();
TokenManager.removeUser();
window.location.href = '/index.html';
},
getMe: () => apiRequest('/auth/me'),
getCurrentUser: () => apiRequest('/auth/me'),
createUser: (userData) => apiRequest('/auth/users', {
method: 'POST',
body: JSON.stringify(userData)
}),
getUsers: () => {
console.log('🔍 AuthAPI.getUsers 호출 - 엔드포인트: /auth/users');
return apiRequest('/auth/users');
},
updateUser: (userId, userData) => apiRequest(`/auth/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(userData)
}),
deleteUser: (userId) => apiRequest(`/auth/users/${userId}`, {
method: 'DELETE'
}),
changePassword: (currentPassword, newPassword) => apiRequest('/auth/change-password', {
method: 'POST',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
}),
resetPassword: (userId, newPassword = '000000') => apiRequest(`/auth/users/${userId}/reset-password`, {
method: 'POST',
body: JSON.stringify({
new_password: newPassword
})
}),
// 부서 목록 가져오기
getDepartments: () => [
{ value: 'production', label: '생산' },
{ value: 'quality', label: '품질' },
{ value: 'purchasing', label: '구매' },
{ value: 'design', label: '설계' },
{ value: 'sales', label: '영업' }
],
// 부서명 변환
getDepartmentLabel: (departmentValue) => {
const departments = AuthAPI.getDepartments();
const dept = departments.find(d => d.value === departmentValue);
return dept ? dept.label : departmentValue || '미지정';
}
};
// Issues API
const IssuesAPI = {
create: async (issueData) => {
// photos 배열 처리 (최대 2장)
const dataToSend = {
category: issueData.category,
description: issueData.description,
project_id: issueData.project_id,
photo: issueData.photos && issueData.photos.length > 0 ? issueData.photos[0] : null,
photo2: issueData.photos && issueData.photos.length > 1 ? issueData.photos[1] : null
};
return apiRequest('/issues/', {
method: 'POST',
body: JSON.stringify(dataToSend)
});
},
getAll: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
return apiRequest(`/issues/${queryString ? '?' + queryString : ''}`);
},
get: (id) => apiRequest(`/issues/${id}`),
update: (id, issueData) => apiRequest(`/issues/${id}`, {
method: 'PUT',
body: JSON.stringify(issueData)
}),
delete: (id) => apiRequest(`/issues/${id}`, {
method: 'DELETE'
}),
getStats: () => apiRequest('/issues/stats/summary')
};
// Daily Work API
const DailyWorkAPI = {
create: (workData) => apiRequest('/daily-work/', {
method: 'POST',
body: JSON.stringify(workData)
}),
getAll: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
return apiRequest(`/daily-work/${queryString ? '?' + queryString : ''}`);
},
get: (id) => apiRequest(`/daily-work/${id}`),
update: (id, workData) => apiRequest(`/daily-work/${id}`, {
method: 'PUT',
body: JSON.stringify(workData)
}),
delete: (id) => apiRequest(`/daily-work/${id}`, {
method: 'DELETE'
}),
getStats: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
return apiRequest(`/daily-work/stats/summary${queryString ? '?' + queryString : ''}`);
}
};
// Reports API
const ReportsAPI = {
getSummary: (startDate, endDate) => apiRequest('/reports/summary', {
method: 'POST',
body: JSON.stringify({
start_date: startDate,
end_date: endDate
})
}),
getIssues: (startDate, endDate) => {
const params = new URLSearchParams({
start_date: startDate,
end_date: endDate
}).toString();
return apiRequest(`/reports/issues?${params}`);
},
getDailyWorks: (startDate, endDate) => {
const params = new URLSearchParams({
start_date: startDate,
end_date: endDate
}).toString();
return apiRequest(`/reports/daily-works?${params}`);
}
};
// 권한 체크
function checkAuth() {
const user = TokenManager.getUser();
if (!user) {
window.location.href = '/index.html';
return null;
}
return user;
}
function checkAdminAuth() {
const user = checkAuth();
if (user && user.role !== 'admin') {
alert('관리자 권한이 필요합니다.');
window.location.href = '/index.html';
return null;
}
return user;
}
// 페이지 접근 권한 체크 함수 (새로 추가)
function checkPageAccess(pageName) {
const user = checkAuth();
if (!user) return null;
// admin은 모든 페이지 접근 가능
if (user.role === 'admin') return user;
// 페이지별 권한 체크는 pagePermissionManager에서 처리
if (window.pagePermissionManager && !window.pagePermissionManager.canAccessPage(pageName)) {
alert('이 페이지에 접근할 권한이 없습니다.');
window.location.href = '/index.html';
return null;
}
return user;
}
// 프로젝트 API
const ProjectsAPI = {
getAll: (activeOnly = false) => {
const params = activeOnly ? '?active_only=true' : '';
return apiRequest(`/projects/${params}`);
},
get: (id) => apiRequest(`/projects/${id}`),
create: (projectData) => apiRequest('/projects/', {
method: 'POST',
body: JSON.stringify(projectData)
}),
update: (id, projectData) => apiRequest(`/projects/${id}`, {
method: 'PUT',
body: JSON.stringify(projectData)
}),
delete: (id) => apiRequest(`/projects/${id}`, {
method: 'DELETE'
})
};