feat: 공동작업을 위한 프로젝트 구조 최적화 및 가이드 배포

This commit is contained in:
thlee3 committed 2026-04-13 17:29:13 +09:00
1 parent 6bca7beb8e
commit 6a038f0a64
50 files changed
+2874 -1244

No files matched your search

@@ -0,0 +1,168 @@
import { useState } from 'react'
import { idcServers, idcStorages, IdcServer } from '../data/idcData'
import ServerDetailModal from './ServerDetailModal'
const AssetManagementView = () => {
const [viewMode, setViewMode] = useState<'server' | 'storage'>('server')
const [selectedServer, setSelectedServer] = useState<IdcServer | null>(null)
return (
<div className="asset-management" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div className="content-header">
<div className="content-title">전산자산관리대장 (IDC)</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
className={`btn ${viewMode === 'server' ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setViewMode('server')}
>
서버 목록
</button>
<button
className={`btn ${viewMode === 'storage' ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setViewMode('storage')}
>
스토리지 목록
</button>
</div>
</div>
<div className="table-container">
<div style={{ padding: '24px 0 16px 0' }}>
<h3 style={{ fontSize: '1.125rem', fontWeight: 600, color: 'var(--primary-color)', margin: 0 }}>
{viewMode === 'server' ? 'IDC 서버 상세 정보' : 'IDC 스토리지 상세 정보'}
</h3>
</div>
{viewMode === 'server' ? (
<table className="data-table">
<thead>
<tr>
<th>회사</th>
<th>서버번호</th>
<th>구분</th>
<th>설치위치</th>
<th>관리자</th>
<th>IP 주소</th>
<th>접속 정보</th>
<th>H/W 사양</th>
<th>OS</th>
<th>구매일</th>
</tr>
</thead>
<tbody>
{idcServers.map((server) => (
<tr key={server.serverNo} onClick={() => setSelectedServer(server)} style={{ cursor: 'pointer' }}>
<td style={{ fontWeight: 600 }}>{server.company}</td>
<td style={{ color: 'var(--primary-color)', fontWeight: 500 }}>{server.serverNo}</td>
<td>
<div>{server.category}</div>
{server.remarks && <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{server.remarks}</div>}
</td>
<td>{server.location}</td>
<td>
<div style={{ fontWeight: 500 }}>{server.managerPrimary ? `정: ${server.managerPrimary}` : '정: -'}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{server.managerSecondary ? `부: ${server.managerSecondary}` : '부: -'}</div>
</td>
<td>
<div>{server.ip1}</div>
{server.ip2 && <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{server.ip2}</div>}
</td>
<td>
{server.remoteAccess.map((access, idx) => (
<div key={idx} style={{
marginBottom: idx < server.remoteAccess.length - 1 ? '8px' : 0,
display: 'flex',
flexDirection: 'column',
gap: '2px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ fontSize: '0.7rem', backgroundColor: '#f3f4f6', padding: '1px 4px', borderRadius: '2px', color: 'var(--text-muted)', fontWeight: 600 }}>{access.tool}</span>
<span style={{ fontSize: '0.8125rem', fontWeight: 500 }}>{access.id}</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', paddingLeft: '4px', borderLeft: '1px solid var(--border-color)', marginLeft: '4px' }}>
PW: <span style={{ color: 'var(--text-main)' }}>{access.pw}</span>
</div>
</div>
))}
</td>
<td style={{ fontSize: '0.8125rem' }}>
<div style={{ fontWeight: 500 }}>{server.model}</div>
<div style={{ color: 'var(--text-muted)' }}>{server.cpu} / {server.ram}</div>
<div style={{ color: 'var(--text-muted)' }}>{server.storage.join(' + ')}</div>
</td>
<td style={{ fontSize: '0.8125rem' }}>{server.os}</td>
<td style={{ fontSize: '0.8125rem' }}>{server.purchaseDate}</td>
</tr>
))}
</tbody>
</table>
) : (
<table className="data-table">
<thead>
<tr>
<th>회사</th>
<th>서버번호</th>
<th>구분</th>
<th>설치위치</th>
<th>관리자</th>
<th>IP 주소</th>
<th>접속 정보</th>
<th>모델명</th>
<th>용량</th>
<th>구매일</th>
</tr>
</thead>
<tbody>
{idcStorages.map((storage) => (
<tr key={storage.serverNo}>
<td style={{ fontWeight: 600 }}>{storage.company}</td>
<td style={{ color: 'var(--primary-color)', fontWeight: 500 }}>{storage.serverNo}</td>
<td>
<div>{storage.category}</div>
{storage.remarks && <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{storage.remarks}</div>}
</td>
<td>{storage.location}</td>
<td>
<span style={{ fontWeight: 500 }}>정: {storage.managerPrimary}</span>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginLeft: '8px' }}>부: {storage.managerSecondary}</span>
</td>
<td>{storage.ip}</td>
<td>
{storage.remoteAccess.map((access, idx) => (
<div key={idx} style={{
marginBottom: idx < storage.remoteAccess.length - 1 ? '8px' : 0,
display: 'flex',
flexDirection: 'column',
gap: '2px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ fontSize: '0.7rem', backgroundColor: '#f3f4f6', padding: '1px 4px', borderRadius: '2px', color: 'var(--text-muted)', fontWeight: 600 }}>{access.tool}</span>
<span style={{ fontSize: '0.8125rem', fontWeight: 500 }}>{access.id}</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', paddingLeft: '4px', borderLeft: '1px solid var(--border-color)', marginLeft: '4px' }}>
PW: <span style={{ color: 'var(--text-main)' }}>{access.pw}</span>
</div>
</div>
))}
</td>
<td>{storage.model}</td>
<td style={{ fontWeight: 600 }}>{storage.capacity}</td>
<td>{storage.purchaseDate}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{selectedServer && (
<ServerDetailModal
server={selectedServer}
onClose={() => setSelectedServer(null)}
/>
)}
</div>
)
}
export default AssetManagementView
@@ -0,0 +1,51 @@
import { mockCategories } from '../data/mockData'
const DashboardView = () => {
return (
<div>
<div className="content-header">
<div className="content-title">대시보드</div>
</div>
<div className="dashboard-stats">
<div className="stat-card">
<div className="stat-label">전체 자산</div>
<div className="stat-value">8개</div>
</div>
{mockCategories.map(cat => (
<div key={cat.id} className="stat-card">
<div className="stat-label">{cat.name}</div>
<div className="stat-value">{cat.count}개</div>
</div>
))}
</div>
<div className="card">
<h3>최근 변경 내역</h3>
<table className="data-table">
<thead>
<tr>
<th>날짜</th>
<th>내용</th>
<th>사용자</th>
</tr>
</thead>
<tbody>
<tr>
<td>2023-04-11</td>
<td>PC 신규 등록</td>
<td>이관형</td>
</tr>
<tr>
<td>2023-04-10</td>
<td>모니터 부서 할당 변경</td>
<td>관리자</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}
export default DashboardView
@@ -0,0 +1,87 @@
import { useState } from 'react'
import { mockHardwareSpecs, HardwareSpec } from '../data/mockData'
const SpecModal = ({ spec, onClose }: { spec: HardwareSpec, onClose: () => void }) => {
return (
<div style={{
position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'center',
zIndex: 1000
}}>
<div className="card" style={{ width: '600px', maxWidth: '90%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
<h2>상세 사양 정보</h2>
<button className="btn" onClick={onClose}>&times;</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '10px' }}>
<strong>PC명:</strong> <span>{spec.pcName}</span>
<strong>사용자:</strong> <span>{spec.userName}</span>
<strong>부서:</strong> <span>{spec.department}</span>
<strong>OS:</strong> <span>{spec.os}</span>
<strong>CPU:</strong> <span>{spec.cpu}</span>
<strong>Memory:</strong> <span>{spec.memory}</span>
<strong>Disk:</strong> <span>{spec.disk}</span>
<strong>MAC:</strong> <span>{spec.macAddress}</span>
<strong>IP:</strong> <span>{spec.ipAddress}</span>
<strong>Graphic:</strong> <span>{spec.graphicCard}</span>
</div>
<div style={{ marginTop: '20px', textAlign: 'right' }}>
<button className="btn btn-primary" onClick={onClose}>닫기</button>
</div>
</div>
</div>
)
}
const HardwareManagementView = () => {
const [selectedSpec, setSelectedSpec] = useState<HardwareSpec | null>(null)
return (
<div>
<div className="content-header">
<div className="content-title">H/W 사양 정보</div>
</div>
<table className="data-table">
<thead>
<tr>
<th>PC명</th>
<th>부서</th>
<th>사용자</th>
<th>OS</th>
<th>CPU</th>
<th>IP주소</th>
<th>상세</th>
</tr>
</thead>
<tbody>
{mockHardwareSpecs.map(spec => (
<tr key={spec.id}>
<td>{spec.pcName}</td>
<td>{spec.department}</td>
<td>{spec.userName}</td>
<td>{spec.os.split(' ')[2]}</td>
<td title={spec.cpu}>{spec.cpu.split('@')[0]}</td>
<td>{spec.ipAddress}</td>
<td>
<button
className="btn btn-primary"
style={{ padding: '4px 8px', fontSize: '0.8rem' }}
onClick={() => setSelectedSpec(spec)}
>
보기
</button>
</td>
</tr>
))}
</tbody>
</table>
{selectedSpec && (
<SpecModal spec={selectedSpec} onClose={() => setSelectedSpec(null)} />
)}
</div>
)
}
export default HardwareManagementView
@@ -0,0 +1,144 @@
import React, { useEffect } from 'react';
import { IdcServer } from '../data/idcData';
interface ServerDetailModalProps {
server: IdcServer;
onClose: () => void;
}
const ServerDetailModal: React.FC<ServerDetailModalProps> = ({ server, onClose }) => {
// ESC 키로 모달 닫기
useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleEsc);
return () => {
window.removeEventListener('keydown', handleEsc);
};
}, [onClose]);
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 style={{ margin: 0, fontSize: '1.125rem', fontWeight: 600 }}>{server.category} ({server.serverNo})</h2>
<button className="modal-close-btn" onClick={onClose} aria-label="Close modal">&times;</button>
</div>
<div className="modal-body">
<div className="detail-grid">
{/* Row 1 */}
<div className="detail-item">
<label>회사 구분</label>
<div className="detail-value">{server.company}</div>
</div>
<div className="detail-item">
<label>서버 번호</label>
<div className="detail-value" style={{ color: 'var(--primary-color)', fontWeight: 600 }}>{server.serverNo}</div>
</div>
{/* Row 2 */}
<div className="detail-item">
<label>자산명(용도)</label>
<div className="detail-value">{server.category}</div>
</div>
<div className="detail-item">
<label>설치 위치</label>
<div className="detail-value">{server.location}</div>
</div>
{/* Row 3: 관리자 추가 */}
<div className="detail-item full-width">
<label>관리 담당자</label>
<div className="detail-value">
<span style={{ fontWeight: 600, color: 'var(--text-main)' }}>정: {server.managerPrimary}</span>
<span style={{ marginLeft: '16px', color: 'var(--text-muted)' }}>부: {server.managerSecondary}</span>
</div>
</div>
{/* Row 4 */}
<div className="detail-item">
<label>IP 주소 1</label>
<div className="detail-value">{server.ip1 || '-'}</div>
</div>
<div className="detail-item">
<label>IP 주소 2</label>
<div className="detail-value">{server.ip2 || '-'}</div>
</div>
{/* Row 5 */}
<div className="detail-item full-width">
<label>원격 접속 정보</label>
<div className="detail-value">
{server.remoteAccess.length > 0 ? (
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
{server.remoteAccess.map((access, idx) => (
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '4px 8px', backgroundColor: 'var(--bg-muted)', borderRadius: '4px', border: '1px solid var(--border-color)' }}>
<span style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-muted)' }}>{access.tool}</span>
<span style={{ fontWeight: 500 }}>{access.id}</span>
<span style={{ color: 'var(--border-color)' }}>|</span>
<span>PW: <span style={{ color: 'var(--text-main)' }}>{access.pw}</span></span>
</div>
))}
</div>
) : '-'}
</div>
</div>
{/* Row 6 */}
<div className="detail-item">
<label>제조사 및 모델명</label>
<div className="detail-value">{server.model || '-'}</div>
</div>
<div className="detail-item">
<label>OS</label>
<div className="detail-value">{server.os || '-'}</div>
</div>
{/* Row 7 */}
<div className="detail-item">
<label>CPU</label>
<div className="detail-value">{server.cpu || '-'}</div>
</div>
<div className="detail-item">
<label>RAM</label>
<div className="detail-value">{server.ram || '-'}</div>
</div>
{/* Row 8 */}
<div className="detail-item full-width">
<label>Storage (디스크 구성)</label>
<div className="detail-value">{server.storage.length > 0 ? server.storage.join(' + ') : '-'}</div>
</div>
{/* Row 9 */}
<div className="detail-item">
<label>구매일자</label>
<div className="detail-value">{server.purchaseDate || '-'}</div>
</div>
<div className="detail-item">
<label>모니터링 여부</label>
<div className="detail-value">{server.monitoring || '-'}</div>
</div>
{/* Row 10 */}
<div className="detail-item full-width">
<label>비고 및 특이사항</label>
<div className="detail-value" style={{ minHeight: '40px' }}>{server.remarks || '-'}</div>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-outline" onClick={onClose} style={{ marginRight: '8px' }}>닫기</button>
<button className="btn btn-primary" onClick={onClose}>수정(저장)</button>
</div>
</div>
</div>
);
};
export default ServerDetailModal;