feat: 자재 관리 페이지 대규모 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산)
- UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인)
- 자재별 그룹핑 로직 개선:
  * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시
  * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등)
  * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시
  * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계)
  * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시
  * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑
- 전체 카테고리 버튼 제거 (표시 복잡도 감소)
- 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
This commit is contained in:
Hyungi Ahn
2025-09-09 09:24:45 +09:00
parent 4f8e395f87
commit 83b90ef05c
101 changed files with 10841 additions and 4813 deletions

View File

@@ -0,0 +1,455 @@
import React, { useState, useEffect } from 'react';
import api from '../api';
const SystemSettingsPage = ({ onNavigate, user }) => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showCreateForm, setShowCreateForm] = useState(false);
const [newUser, setNewUser] = useState({
username: '',
email: '',
password: '',
full_name: '',
role: 'user'
});
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
try {
setLoading(true);
const response = await api.get('/auth/users');
if (response.data.success) {
setUsers(response.data.users);
}
} catch (err) {
console.error('사용자 목록 로딩 실패:', err);
setError('사용자 목록을 불러오는데 실패했습니다.');
} finally {
setLoading(false);
}
};
const handleCreateUser = async (e) => {
e.preventDefault();
if (!newUser.username || !newUser.email || !newUser.password) {
setError('모든 필수 필드를 입력해주세요.');
return;
}
try {
setLoading(true);
const response = await api.post('/auth/register', newUser);
if (response.data.success) {
alert('사용자가 성공적으로 생성되었습니다.');
setNewUser({
username: '',
email: '',
password: '',
full_name: '',
role: 'user'
});
setShowCreateForm(false);
loadUsers();
}
} catch (err) {
console.error('사용자 생성 실패:', err);
setError(err.response?.data?.detail || '사용자 생성에 실패했습니다.');
} finally {
setLoading(false);
}
};
const handleDeleteUser = async (userId) => {
if (!confirm('정말로 이 사용자를 삭제하시겠습니까?')) {
return;
}
try {
setLoading(true);
const response = await api.delete(`/auth/users/${userId}`);
if (response.data.success) {
alert('사용자가 삭제되었습니다.');
loadUsers();
}
} catch (err) {
console.error('사용자 삭제 실패:', err);
setError('사용자 삭제에 실패했습니다.');
} finally {
setLoading(false);
}
};
const getRoleDisplay = (role) => {
switch (role) {
case 'admin': return '관리자';
case 'manager': return '매니저';
case 'user': return '사용자';
default: return role;
}
};
const getRoleBadgeColor = (role) => {
switch (role) {
case 'admin': return '#dc2626';
case 'manager': return '#ea580c';
case 'user': return '#059669';
default: return '#6b7280';
}
};
// 관리자 권한 확인
if (user?.role !== 'admin') {
return (
<div style={{ padding: '32px', textAlign: 'center' }}>
<h2 style={{ color: '#dc2626', marginBottom: '16px' }}>접근 권한이 없습니다</h2>
<p style={{ color: '#6b7280', marginBottom: '24px' }}>
시스템 설정은 관리자만 접근할 있습니다.
</p>
<button
onClick={() => onNavigate('dashboard')}
style={{
background: '#4299e1',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '12px 24px',
cursor: 'pointer'
}}
>
대시보드로 돌아가기
</button>
</div>
);
}
return (
<div style={{ padding: '32px', maxWidth: '1200px', margin: '0 auto' }}>
{/* 헤더 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '32px'
}}>
<div>
<h1 style={{ fontSize: '28px', fontWeight: '700', color: '#2d3748', marginBottom: '8px' }}>
시스템 설정
</h1>
<p style={{ color: '#718096', fontSize: '16px' }}>
사용자 계정 관리 시스템 설정
</p>
</div>
<button
onClick={() => onNavigate('dashboard')}
style={{
background: '#e2e8f0',
color: '#4a5568',
border: 'none',
borderRadius: '6px',
padding: '12px 20px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
대시보드
</button>
</div>
{error && (
<div style={{
background: '#fed7d7',
color: '#c53030',
padding: '12px 16px',
borderRadius: '6px',
marginBottom: '24px'
}}>
{error}
</div>
)}
{/* 사용자 관리 섹션 */}
<div style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.07)',
border: '1px solid #e2e8f0',
marginBottom: '24px'
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '24px'
}}>
<h2 style={{ fontSize: '20px', fontWeight: '600', color: '#2d3748' }}>
👥 사용자 관리
</h2>
<button
onClick={() => setShowCreateForm(!showCreateForm)}
style={{
background: '#38a169',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '10px 16px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
+ 사용자 생성
</button>
</div>
{/* 사용자 생성 폼 */}
{showCreateForm && (
<div style={{
background: '#f7fafc',
padding: '20px',
borderRadius: '8px',
marginBottom: '24px',
border: '1px solid #e2e8f0'
}}>
<h3 style={{ fontSize: '16px', fontWeight: '600', marginBottom: '16px' }}>
사용자 생성
</h3>
<form onSubmit={handleCreateUser}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
사용자명 *
</label>
<input
type="text"
value={newUser.username}
onChange={(e) => setNewUser({...newUser, username: e.target.value})}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '14px'
}}
required
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
이메일 *
</label>
<input
type="email"
value={newUser.email}
onChange={(e) => setNewUser({...newUser, email: e.target.value})}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '14px'
}}
required
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
비밀번호 *
</label>
<input
type="password"
value={newUser.password}
onChange={(e) => setNewUser({...newUser, password: e.target.value})}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '14px'
}}
required
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
전체 이름
</label>
<input
type="text"
value={newUser.full_name}
onChange={(e) => setNewUser({...newUser, full_name: e.target.value})}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '14px'
}}
/>
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
권한
</label>
<select
value={newUser.role}
onChange={(e) => setNewUser({...newUser, role: e.target.value})}
style={{
width: '200px',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '14px'
}}
>
<option value="user">사용자</option>
<option value="manager">매니저</option>
<option value="admin">관리자</option>
</select>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
type="submit"
disabled={loading}
style={{
background: '#38a169',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '10px 16px',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
{loading ? '생성 중...' : '사용자 생성'}
</button>
<button
type="button"
onClick={() => setShowCreateForm(false)}
style={{
background: '#e2e8f0',
color: '#4a5568',
border: 'none',
borderRadius: '6px',
padding: '10px 16px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
취소
</button>
</div>
</form>
</div>
)}
{/* 사용자 목록 */}
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '16px', color: '#718096' }}>로딩 ...</div>
</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '2px solid #e2e8f0' }}>
<th style={{ padding: '12px', textAlign: 'left', fontWeight: '600', color: '#4a5568' }}>
사용자명
</th>
<th style={{ padding: '12px', textAlign: 'left', fontWeight: '600', color: '#4a5568' }}>
이메일
</th>
<th style={{ padding: '12px', textAlign: 'left', fontWeight: '600', color: '#4a5568' }}>
전체 이름
</th>
<th style={{ padding: '12px', textAlign: 'center', fontWeight: '600', color: '#4a5568' }}>
권한
</th>
<th style={{ padding: '12px', textAlign: 'center', fontWeight: '600', color: '#4a5568' }}>
상태
</th>
<th style={{ padding: '12px', textAlign: 'center', fontWeight: '600', color: '#4a5568' }}>
작업
</th>
</tr>
</thead>
<tbody>
{users.map((userItem) => (
<tr key={userItem.id} style={{ borderBottom: '1px solid #e2e8f0' }}>
<td style={{ padding: '12px', fontWeight: '500' }}>
{userItem.username}
</td>
<td style={{ padding: '12px', color: '#4a5568' }}>
{userItem.email}
</td>
<td style={{ padding: '12px', color: '#4a5568' }}>
{userItem.full_name || '-'}
</td>
<td style={{ padding: '12px', textAlign: 'center' }}>
<span style={{
background: getRoleBadgeColor(userItem.role),
color: 'white',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: '600'
}}>
{getRoleDisplay(userItem.role)}
</span>
</td>
<td style={{ padding: '12px', textAlign: 'center' }}>
<span style={{
background: userItem.is_active ? '#d1fae5' : '#fee2e2',
color: userItem.is_active ? '#065f46' : '#dc2626',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: '600'
}}>
{userItem.is_active ? '활성' : '비활성'}
</span>
</td>
<td style={{ padding: '12px', textAlign: 'center' }}>
{userItem.id !== user?.id && (
<button
onClick={() => handleDeleteUser(userItem.id)}
style={{
background: '#dc2626',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '6px 12px',
cursor: 'pointer',
fontSize: '12px',
fontWeight: '600'
}}
>
삭제
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
};
export default SystemSettingsPage;