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

This commit is contained in:
2026-04-13 17:29:13 +09:00
parent 6bca7beb8e
commit 6a038f0a64
50 changed files with 2874 additions and 1244 deletions

View File

@@ -1,15 +0,0 @@
{
"mcpServers": {
"gitea": {
"command": "npx",
"args": [
"-y",
"@andrebuzeli/git-mcp@latest"
],
"env": {
"GITEA_HOST": "https://gitea.hmac.kr",
"GITEA_ACCESS_TOKEN": "0318de8265b9cbabc5e70773e94c59807ec3ad8a"
}
}
}
}

8
.gitignore vendored
View File

@@ -1 +1,7 @@
node_modules/ node_modules/
.gemini/
.env
dist/
*.log
.DS_Store
Thumbs.db

49
PROJECT_GUIDE.md Normal file
View File

@@ -0,0 +1,49 @@
# 📗 ITAM 프로젝트 구성 및 협업 가이드
본 문서는 ITAM(IT Asset Management System)의 프로젝트 구조와 공동 작업을 위한 가이드를 제공합니다.
## 1. 프로젝트 아키텍처 개요
ITAM은 **중앙 상태 관리(Centralized State)**와 **컴포넌트 기반 UI(Component-based UI)** 구조로 설계되었습니다. 모든 UI와 비즈니스 로직은 기능별로 독립된 파일로 분리되어 있어, 여러 작업자가 충돌 없이 동시에 개발할 수 있습니다.
## 2. 핵심 디렉토리 구조 및 역할
### 🏗️ 제어 로직 (Core)
* **`src/main.ts`**: 시스템 관제탑. 전체 컴포넌트 초기화 및 메인 렌더링 흐름을 제어합니다.
* **`src/state.ts`**: **전역 데이터 창고**. 자산 데이터(`masterData`)와 현재 탭 상태를 중앙에서 관리합니다. 데이터 구조 변경 시 가장 먼저 확인해야 할 파일입니다.
### 🛠️ 상세 페이지 및 모달 (Modals)
모든 자산의 추가/수정/삭제 로직은 `src/components/Modal/` 폴더 내에 독립적으로 구성되어 있습니다.
* **`BaseModal.ts`**: 모든 모달의 공통 기능(닫기, ESC 처리, 배경 클릭)을 담당합니다.
* **`PCModal.ts`**: 개인PC 전용 상세 정보 및 사양 관리.
* **`HWModal.ts`**: 서버, 전산비품 자산 상세 정보 관리.
* **`StorageModal.ts`**: 스토리지(NAS/DAS) 특화 필드 및 정보 관리.
* **`SWModal.ts`**: 소프트웨어 라이선스 기본 정보 관리.
* **`SWUserModal.ts`**: **복잡한 로직 영역**. 소프트웨어별 사용자 할당/해제 및 매핑 로직을 담당합니다.
### 📊 화면 렌더링 (Views)
* **`src/views/DashboardView.ts`**: HW/SW 현황 통계 및 요약 차트 화면을 렌더링합니다.
* **`src/views/AssetTableView.ts`**: 각 카테고리별 자산 목록 테이블을 렌더링합니다.
## 3. 공동 작업 가이드 (협업 전략)
본 프로젝트는 파일 단위로 역할이 명확히 나뉘어 있어, **담당 영역에 따라 독립적인 작업이 가능**합니다.
### 👤 담당자별 권장 작업 영역
| 작업 대상 | 담당 파일 (Primary) | 설명 |
| :--- | :--- | :--- |
| **하드웨어(HW) 담당** | `PCModal.ts`, `HWModal.ts`, `StorageModal.ts` | 하드웨어 상세 페이지 및 사양 관리 로직 개발 |
| **소프트웨어(SW) 담당** | `SWModal.ts`, `SWUserModal.ts` | 소프트웨어 정보 및 사용자 할당 시스템 개발 |
### 🤝 공통 영역 및 주의사항
아래 파일들은 두 담당자가 공통으로 사용하는 영역이므로, 수정 시 Git 충돌에 유의하고 소통이 필요합니다.
1. **`src/state.ts`**: 데이터 인터페이스(Interface)를 변경할 경우.
2. **`src/views/AssetTableView.ts`**: 목록 테이블의 공통 스타일이나 HW/SW 테이블 구조를 변경할 경우.
3. **`src/views/DashboardView.ts`**: 대시보드 통계 알고리즘을 변경할 경우.
## 4. 데이터 흐름 (Data Flow)
1. **데이터 조회**: `AssetTableView`에서 항목 클릭 → 담당 모달의 `openModal(asset)` 호출 → 폼 바인딩.
2. **데이터 저장**: 모달에서 '저장' 버튼 클릭 → `state.ts`의 전역 상태 업데이트 → `main.ts``renderContent()` 호출 → 전체 화면 즉시 갱신.
---
**Tip**: 새로운 기능을 추가할 때는 `main.ts`에 코드를 직접 작성하지 말고, 적절한 폴더 아래에 새 파일을 만들어 `import` 하시기 바랍니다.

57
backup_temp/README.md Normal file
View File

@@ -0,0 +1,57 @@
# 🛠️ 개발 및 관리 규칙 (Strict Development Rules)
1. **언어 설정**: 영어로 생각하되, 모든 답변은 **한국어**로 작성한다.
2. **임의 수정 절대 금지 (Zero-Arbitrary Change)**:
- 사용자가 명시적으로 지시한 부분 외에는 **단 한 줄의 코드도, 그 어떤 파일도 임의로 수정, 정리, 리팩토링하지 않는다.**
- 지시받지 않은 다른 파트의 코드는 절대 건드리지 않으며, 영향 범위가 요청 범위를 벗어나지 않도록 '외과 수술식(Surgical) 수정'을 원칙으로 한다.
3. **개선 작업 절차 (Test-First Approach)**:
- 사용자가 개선(Refactoring, Optimization 등)을 지시한 경우, **수정 전 현재 시스템이 정상적으로 잘 작동하는지 먼저 전수 확인**한다.
- 기존 동작 방식과 성능을 기준(Baseline)으로 삼고, 수정 후에도 **기존의 모든 기능이 무결하게 유지되는지 반드시 테스트하여 입증**한다.
- 검증 결과를 바탕으로 "무엇을, 왜, 어떻게" 바꿀지 상세 보고 후, 사용자로부터 **'진행시켜'** 승인을 얻은 뒤에만 집행한다.
4. **선보고 후승인**: 모든 기능 수정 및 코드 변경 전에는 예상 방안을 먼저 보고하고 승인 절차를 거친다.
---
### 🚀 서버 구동 및 외부 접속 규칙 (Server Run & External Access)
1. **포트 고정**: 개발 서버는 반드시 **8080** 포트를 사용한다. (`vite.config.ts` 설정 준수)
2. **외부 접속 허용 (Host)**: 사무실 내 타 직원이 접속할 수 있도록 `--host` 모드로 구동한다.
3. **구동 명령어**:
```bash
npm run dev
```
* 해당 명령어 실행 시 `0.0.0.0` 또는 `Network: http://[내-IP]:8080/` 경로로 타인 접속이 가능하다.
4. **IP 확인 방법**:
* Windows: `ipconfig` 명령어로 'IPv4 주소' 확인 후 공유.
---
### 🎨 ITAM 시스템 디자인 가이드 (Design Guide)
1. **디자인 철학 (Design Philosophy)**
* **Minimalist & Border-based**: 불필요한 박스(Card) 사용을 최소화하고, 정보의 구분은 간결한 라인(Border/Divider)을 활용하여 시각적 피로도를 낮춥니다.
* **Professional Achromatic**: 무채색(Black, White, Grey)을 기본으로 하여 정돈된 업무 환경을 제공합니다.
* **Green Accent**: 블루 대신 짙은 그린(`#1E5149`)을 포인트 컬러로 사용하여 차분한 전문성을 강조합니다.
2. **타이포그래피 (Typography)**
* **Font Family**: `Pretendard` (전역 적용)
* **Letter Spacing**: `-0.02em` (약 -2%) 적용. 자간을 좁게 설정하여 밀도 있고 세련된 가독성을 확보합니다.
* **Weights**: 400(Regular), 500(Medium), 600(SemiBold), 700(Bold).
* **Date Format (날짜 표기 규칙)**: 시스템 내 모든 날짜는 `YYYY.MM.DD` 형식을 기본으로 사용합니다. (예: 2026.04.10)
3. **컬러 팔레트 (Color Palette)**
* **Point Color**: `#1E5149` (Deep Green) - 강조, 활성화 상태, 주요 액션 버튼.
* **Text**: Main(`#111827` - Near Black), Muted(`#6B7280` - Grey).
* **Border/Divider**: `#E5E7EB` (Light Grey) - 정보 구분을 위한 얇은 실선.
* **Background**: `#FFFFFF` (White) / `#F9FAFB` (Off White).
4. **레이아웃 및 컴포넌트 규칙 (Layout Rules)**
* **Box-less Design**: 꼭 필요한 정보 묶음(데이터 그룹화 등)이 아니면 박스 형태의 테두리나 배경 사용을 지양합니다.
* **Line-based Division**: 섹션 간의 구분은 1px 두께의 얇은 실선(Border)을 통해 명확히 합니다.
* **Table**: 배경색이나 화려한 효과 없이 행(Row) 간의 얇은 구분선만 사용하여 데이터 본연에 집중하게 합니다.
* **Input/Button**: 입력 필드와 버튼은 최소한의 보더와 포인트 컬러만 사용하여 정갈하게 표현합니다.
* **Modal (모달 공통 규칙)**:
* **Header**: 짙은 그린(`#1E5149`) 배경에 화이트 텍스트를 사용하며, 우측 상단에 명확한 'X' 닫기 버튼을 배치합니다.
* **Interaction**: 사용자의 편의를 위해 `ESC` 키를 누르거나 모달 바깥 영역(Overlay)을 클릭하면 모달이 닫히도록 구현합니다.
* **Layout**: `detail.png` 기준의 2열 그리드 시스템을 권장하며, 하단 우측에 액션 버튼(닫기, 저장 등)을 배치합니다.

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

BIN
backup_temp/detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

13
backup_temp/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css" />
<title>ITAM - IT Asset Management</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

32
backup_temp/plan.md Normal file
View File

@@ -0,0 +1,32 @@
# 📑 ITAM 시스템 통합 구축 기획서 (plan.md)
## 1. 프로젝트 목적 (Objective)
본 시스템은 **'전산자산번호 부여방안'** 표준 가이드에 따라 사내 모든 전산 자산을 통합 관리하는 것을 목적으로 한다. 분산된 자산 정보를 일원화된 번호 체계로 시스템화하여 직관적인 식별과 정밀한 이력 추적이 가능한 플랫폼을 지향한다.
## 2. 자산번호 체계 적용 (Standardization)
검토안에 따라 모든 자산은 다음과 같은 규칙으로 고유 번호를 부여받으며, 시스템의 핵심 Key로 활용된다.
* **번호 생성 규칙**: `[구매법인]-[설치위치]-[자산종류]-[일련번호(구매연월+3자리)]`
* *예시: 삼안에서 2025년 4월에 구매한 IDC 서버 → `SM-IDC-SVR-202504001`*
* **코드 정의**:
* **구매법인**: BR(바론), SM(삼안), HM(한맥), JH(장헌), HL(한라), PTC(PTC) 등
* **설치위치**: TDC(센터 서버실), HBD(한맥빌딩), IDC(IDC), UBD(유니온 빌딩), NBD(뉴코아 빌딩) 등
* **자산종류**: SVR(서버), PC(PC), STO(스토리지), NAS(NAS), DAS(DAS), HDD(하드) 등
## 3. 기획 의도 및 시스템 가치 (Core Intent)
1. **식별 용이성 확보**: 번호만으로도 법인, 위치, 종류, 도입 시기를 즉시 파악할 수 있는 시스템 UI를 제공한다.
2. **전사 통합 관리 로드맵**:
* **Phase 1**: IDC 서버 및 스토리지 데이터 마이그레이션 (현재 완료 단계)
* **Phase 2**: 센터 서버실(TDC) 및 빌딩별 네트워크 장비 확장
* **Phase 3**: 전사 개인 PC(PC) 및 하드웨어 부품(HDD 등) 통합
* **Phase 4**: 소프트웨어 라이선스와 하드웨어 자산의 매핑 관리
3. **데이터 무결성 유지**: 자산 위치 변경 시 기존 번호를 폐기하고 신규 번호를 부여하는 이력 관리 원칙을 시스템상에서 강제 및 기록한다.
## 4. 시스템 주요 기능 (Key Features)
* **자산 자동 번호 부여**: 입력 폼에서 법인/위치/종류 선택 시 가이드에 따른 자산번호 자동 생성 기능.
* **상세 이력 카드**: 자산번호를 클릭하면 상세 사양, 취득일, 사용자, 현재 상태 및 과거 이동 이력을 모달로 표시.
* **통합 필터링**: 법인별, 위치별, 종류별로 자산을 즉시 분류하여 조회할 수 있는 고성능 테이블 제공.
## 5. 기술 및 디자인 원칙 (Engineering Standards)
* **Design**: `README.md` 가이드를 준수하며, 자산번호가 가장 강조되는 레이아웃을 유지한다.
* **Data Structure**: 향후 DB 전환 시 자산번호의 각 코드(SM, IDC 등)를 정규화하여 관리 효율을 극대화한다.

295
backup_temp/src/App.css Normal file
View File

@@ -0,0 +1,295 @@
:root {
--primary-color: #1E5149;
--primary-hover: #163d37;
--bg-default: #FFFFFF;
--bg-muted: #F9FAFB;
--info-color: #4B5563; /* 무채색 계열로 변경 */
--text-main: #111827;
--text-muted: #6B7280;
--border-color: #E5E7EB;
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
background-color: var(--bg-default);
color: var(--text-main);
letter-spacing: -0.02em;
}
.top-bar {
height: 50px;
background-color: var(--primary-color);
color: white;
display: flex;
align-items: center;
padding: 0 40px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
.top-bar-brand {
font-size: 1rem;
font-weight: 700;
margin-right: 48px;
color: white;
}
.top-bar-menu {
display: flex;
gap: 4px;
height: 100%;
}
.menu-item {
display: flex;
align-items: center;
padding: 0 16px;
cursor: pointer;
transition: all 0.2s;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
font-size: 0.875rem;
height: 100%;
border-bottom: 3px solid transparent;
}
.menu-item:hover {
color: white;
}
.menu-item.active {
color: white;
border-bottom: 3px solid white;
background-color: rgba(255, 255, 255, 0.1);
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
height: calc(100vh - 50px);
overflow: hidden; /* 전체 페이지 스크롤 방지 */
padding: 0; /* 패딩은 내부 요소에서 관리 */
}
.content-header {
padding: 32px 40px 16px 40px;
margin-bottom: 0;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--bg-default);
flex-shrink: 0;
}
.table-container {
flex: 1;
overflow: auto;
padding: 0 40px 40px 40px;
}
.content-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-main);
}
/* Common Table Styles */
.data-table {
width: 100%;
border-collapse: separate; /* sticky border 유지를 위해 separate 사용 */
border-spacing: 0;
font-size: 0.875rem;
}
.data-table th, .data-table td {
padding: 14px 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
white-space: nowrap;
}
.data-table th {
font-weight: 600;
color: var(--text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
background-color: #F8FAFC; /* 헤더 전용 배경색 */
position: sticky;
top: 0;
z-index: 10;
border-bottom: 2px solid var(--border-color);
}
.data-table tr:hover {
background-color: var(--bg-muted);
}
/* Buttons */
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
}
.btn-outline {
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-main);
}
.btn-outline:hover {
background-color: var(--bg-muted);
}
/* Dashboard Stats - Border based */
.dashboard-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0;
margin-bottom: 48px;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
.stat-card {
padding: 24px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
}
.stat-card:last-child {
border-right: none;
}
.stat-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 12px;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
}
.modal-content {
background-color: var(--bg-default);
width: 100%;
max-width: 800px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
max-height: 90vh;
}
.modal-header {
background-color: var(--primary-color);
color: white;
padding: 16px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-close-btn {
background: none;
border: none;
color: white;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
padding: 4px;
opacity: 0.8;
transition: opacity 0.2s;
}
.modal-close-btn:hover {
opacity: 1;
}
.modal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 24px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.detail-item.full-width {
grid-column: 1 / -1;
}
.detail-item label {
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-main);
}
.detail-value {
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.875rem;
color: var(--text-muted);
background-color: var(--bg-default);
min-height: 20px;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
background-color: var(--bg-muted);
}

51
backup_temp/src/App.tsx Normal file
View File

@@ -0,0 +1,51 @@
import { useState } from 'react'
import './App.css'
import AssetManagementView from './components/AssetManagementView'
import HardwareManagementView from './components/HardwareManagementView'
function App() {
const [activeMenu, setActiveMenu] = useState('assets')
const renderContent = () => {
switch (activeMenu) {
case 'assets':
return <AssetManagementView />
case 'hardware':
return <HardwareManagementView />
default:
return <AssetManagementView />
}
}
return (
<div className="app-container">
<header className="top-bar">
<div className="top-bar-brand">
ITAM System
</div>
<nav className="top-bar-menu">
<div
className={`menu-item ${activeMenu === 'assets' ? 'active' : ''}`}
onClick={() => setActiveMenu('assets')}
>
</div>
<div
className={`menu-item ${activeMenu === 'hardware' ? 'active' : ''}`}
onClick={() => setActiveMenu('hardware')}
>
H/W
</div>
<div className="menu-item">S/W </div>
<div className="menu-item"> </div>
<div className="menu-item"> </div>
</nav>
</header>
<main className="main-content">
{renderContent()}
</main>
</div>
)
}
export default App

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1,608 @@
export interface RemoteAccess {
tool: string;
ip?: string;
id: string;
pw: string;
}
export interface IdcServer {
company: string;
serverNo: string;
category: string;
remarks: string;
location: string;
managerPrimary: string;
managerSecondary: string;
ip1: string;
ip2: string;
remoteAccess: RemoteAccess[];
monitoring: string;
serverIdMatch: string;
model: string;
os: string;
cpu: string;
ram: string;
storage: string[];
purchaseDate: string;
}
export interface IdcStorage {
company: string;
serverNo: string;
category: string;
remarks: string;
location: string;
managerPrimary: string;
managerSecondary: string;
ip: string;
managementIp?: string;
remoteAccess: RemoteAccess[];
model: string;
capacity: string;
purchaseDate: string;
}
export const idcServers: IdcServer[] = [
{
company: "한맥",
serverNo: "hm-idc-001",
category: "한맥 인트라넷",
remarks: "",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.70",
ip2: "192.168.10.5",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "samanerp1!" },
{ tool: "Remote Util", id: "211.206.127.70", pw: "1234아이티!" }
],
monitoring: "win exp, raid X",
serverIdMatch: "srv07d330084",
model: "HPE ProLiant DL360 Gen10",
os: "Windows Server 2016",
cpu: "intel xeon silver4110 CPU @2.10GHz",
ram: "32GB",
storage: ["280GB", "2.7TB"],
purchaseDate: "2020.12.10"
},
{
company: "한맥",
serverNo: "hm-idc-002",
category: "한맥 인트라넷 예비",
remarks: "단가, 입사자지원 서버 (스마트 건설 용도 구매)",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.78",
ip2: "192.168.10.13",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "Hanmac2141!" }
],
monitoring: "win exp, raid X",
serverIdMatch: "srcff5294c84",
model: "HPE ProLiant DL360 Gen10",
os: "Windows Server 2019",
cpu: "intel xeon silver4214R CPU @2.40GHz",
ram: "32GB",
storage: ["280GB", "2.7TB"],
purchaseDate: ""
},
{
company: "삼안",
serverNo: "sa-idc-001",
category: "삼안 인트라넷",
remarks: "",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "118.220.172.237",
ip2: "erp.samaneng.com",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "samanerp1!" },
{ tool: "Remote Util", id: "118.220.172.237", pw: "1234아이티!" }
],
monitoring: "O",
serverIdMatch: "newSmintranet",
model: "HPE ProLiant DL360 Gen10",
os: "Windows Server 2016",
cpu: "intel xeon silver4214R CPU @2.40GHz",
ram: "32GB",
storage: ["280GB", "3.27TB"],
purchaseDate: "2019.12.20"
},
{
company: "삼안",
serverNo: "sa-idc-002",
category: "삼안 인트라넷 예비",
remarks: "",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "118.220.172.249",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "samanerp1!" },
{ tool: "Remote Util", id: "678-605-383-130", pw: "1234아이티!" }
],
monitoring: "설치 X",
serverIdMatch: "INTRANET",
model: "HPE ProLiant DL360 GEN9",
os: "Windows Server 2008 R2",
cpu: "Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz",
ram: "32GB",
storage: ["279GB", "2.72TB"],
purchaseDate: ""
},
{
company: "삼안",
serverNo: "sa-idc-003",
category: "SATIS 01",
remarks: "구 SATIS 서버, 세금계산서 발행(회계)",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "118.220.172.228",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "satissg11707808" }
],
monitoring: "설치 X",
serverIdMatch: "satis01",
model: "HPE ProLiant DL380p GEN8",
os: "Windows Server 2008 R2",
cpu: "Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz",
ram: "20GB",
storage: ["100GB", "458GB"],
purchaseDate: ""
},
{
company: "삼안",
serverNo: "sa-idc-004",
category: "SATIS 02",
remarks: "SATIS 리뉴얼 버전 (ERP 서버)",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "118.220.172.229",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "satissg11707808" }
],
monitoring: "설치 X",
serverIdMatch: "satis02",
model: "HPE ProLiant DL380p GEN8",
os: "Windows Server 2008 R2",
cpu: "Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz",
ram: "20GB",
storage: ["100GB", "458GB", "18.1TB"],
purchaseDate: ""
},
{
company: "삼안",
serverNo: "sa-idc-005",
category: "웹 서버",
remarks: "남양주 테스트 서버 (도메인 관리 기능 제거 2026.03.11)",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "samanweb.cafe24.com",
ip2: "118.220.172.195",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "saman+2013+web" }
],
monitoring: "win exp, 포트 안열림",
serverIdMatch: "www",
model: "HPE ProLiant DL380p GEN8",
os: "Windwos Server 2012",
cpu: "Intel(R) Xeon(R) CPU E5-2609 0 @ 2.40GHz",
ram: "16GB",
storage: ["100GB", "230GB", "230GB"],
purchaseDate: ""
},
{
company: "삼안",
serverNo: "sa-idc-006",
category: "PQ DB 서버",
remarks: "",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "118.220.172.231",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "7013ddj10235!" }
],
monitoring: "O",
serverIdMatch: "src5dd67f2ed",
model: "HPE ProLiant DL360 Gen10",
os: "Windows Server 2019",
cpu: "intel xeon silver4210R CPU @2.40GHz",
ram: "32GB",
storage: ["278GB", "2.18TB"],
purchaseDate: "2024.12.16"
},
{
company: "삼안",
serverNo: "sa-idc-007",
category: "Oracle DB 서버",
remarks: "",
location: "서관 202번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "118.220.172.225",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "7013ddj10235!" }
],
monitoring: "win exp, raid X",
serverIdMatch: "SAMAN-DB",
model: "HPE ProLiant DL380 GEN9",
os: "Windows Server 2012",
cpu: "Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz",
ram: "64GB",
storage: ["558GB", "1.09TB", "1.09TB"],
purchaseDate: ""
},
{
company: "삼안",
serverNo: "sa-idc-008",
category: "안전관리",
remarks: "삼안 개발서버2 - AI, SSL, 장헌TBM, 노드",
location: "서관 202번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "1.234.37.171",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "samanerp1!" }
],
monitoring: "연결 X",
serverIdMatch: "",
model: "HPE ProLiant DL380 GEN10",
os: "Windwos Server 2022",
cpu: "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz",
ram: "128GB",
storage: ["278GB", "3.27TB"],
purchaseDate: "2025.04.10"
},
{
company: "삼안",
serverNo: "sa-idc-009",
category: "가족사 공통메뉴",
remarks: "삼안 개발서버1 - QNA, 급여명세서",
location: "서관 202번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "118.220.172.233",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "samanerp1!" }
],
monitoring: "O",
serverIdMatch: "srcc9ac928ee",
model: "HPE ProLiant DL380 GEN10",
os: "Windwos Server 2022",
cpu: "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz",
ram: "128GB",
storage: ["278GB", "3.27TB"],
purchaseDate: "2025.04.10"
},
{
company: "한라",
serverNo: "hl-idc-001",
category: "한라 인트라넷",
remarks: "인트라넷,안전, 운영, MISO 서버로 운영 중(win 2008)",
location: "동관 54번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "1.234.37.143",
ip2: "",
remoteAccess: [
{ tool: "Remote Util", id: "1.234.37.143", pw: "1234dkdlxl!" }
],
monitoring: "설치 X",
serverIdMatch: "",
model: "HPE ProLiant DL360 GEN9",
os: "Windows Server 2008 R2",
cpu: "Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz",
ram: "8GB",
storage: ["299GB", "631GB"],
purchaseDate: ""
},
{
company: "한라",
serverNo: "hl-idc-002",
category: "안전전산화 서버 (디자인팀 웹)",
remarks: "인트라넷 서버 다운 시 백업용 대기",
location: "동관 54번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "1.234.37.144",
ip2: "192.168.20.49",
remoteAccess: [
{ tool: "Remote Util", id: "1.234.37.144", pw: "1234dkdlxl!" }
],
monitoring: "O",
serverIdMatch: "",
model: "HPE ProLiant DL360 GEN9",
os: "Windows Server 2012",
cpu: "Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz",
ram: "8GB",
storage: ["299GB", "631GB"],
purchaseDate: ""
},
{
company: "한라",
serverNo: "hl-idc-003",
category: "개발서버2",
remarks: "PTC 연구비로 구매한 예비서버2",
location: "동관 53번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "192.168.20.171",
ip2: "1.234.37.171",
remoteAccess: [
{ tool: "Remote Util", id: "1.234.37.171", pw: "1234dkdlxl!" }
],
monitoring: "O",
serverIdMatch: "",
model: "HPE ProLiant DL380 Gen10",
os: "Windows Server 2019 Standard",
cpu: "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz",
ram: "32GB",
storage: ["280GB", "1TB"],
purchaseDate: "2022.09.21"
},
{
company: "장헌",
serverNo: "jh-idc-001",
category: "장헌인트라넷",
remarks: "BEPs",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.71",
ip2: "192.168.10.6",
remoteAccess: [
{ tool: "Remote Util", id: "211.206.127.71", pw: "1234dkdlxl!" },
{ tool: "원격데스크탑", id: "administrator", pw: "Hanmac2141!%" }
],
monitoring: "잠금 걸려있음",
serverIdMatch: "src775d3e5df",
model: "HPE ProLiant DL380 GEN10",
os: "Windows Server 2019",
cpu: "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz",
ram: "32GB",
storage: ["280GB", "1TB"],
purchaseDate: "2022.09.21"
},
{
company: "장헌",
serverNo: "jh-idc-002",
category: "장헌 인트라넷 예비",
remarks: "",
location: "동관 53번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "1.234.37.170",
ip2: "192.168.20.170",
remoteAccess: [
{ tool: "Remote Util", id: "1.234.37.170", pw: "1234dkdlxl!" },
{ tool: "원격데스크탑", id: "Administrator", pw: "Hanmac2141!" }
],
monitoring: "원격 X, O",
serverIdMatch: "",
model: "HPE ProLiant DL360 Gen10",
os: "Windows Server 2019",
cpu: "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz",
ram: "32GB",
storage: ["280GB", "1TB"],
purchaseDate: "2022.04.01"
},
{
company: "장헌",
serverNo: "jh-idc-003",
category: "인트라넷(구)",
remarks: "현재는 GIT 백업 으로 사용",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.110",
ip2: "192.168.10.40",
remoteAccess: [
{ tool: "Remote Util", id: "211.206.127.110", pw: "1234dkdlxl!" },
{ tool: "원격데스크탑", id: "User", pw: "Hanmac2141!" }
],
monitoring: "",
serverIdMatch: "",
model: "",
os: "Windows Server 2019",
cpu: "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz",
ram: "",
storage: [],
purchaseDate: ""
},
{
company: "(주)장헌",
serverNo: "jh-idc-004",
category: "(주) 장헌 인트라넷",
remarks: "2025.12.23 IDC 이전 설치",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.76",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "User", pw: "Hanmac2141!%" }
],
monitoring: "win exp, raid X",
serverIdMatch: "DESKTOP-5IL75B7",
model: "",
os: "Windows 10",
cpu: "12th Gen Intel(R) Core(TM) i7-12700F",
ram: "32GB",
storage: ["465GB", "1.81TB"],
purchaseDate: ""
},
{
company: "PTC",
serverNo: "ptc-idc-001",
category: "PTC인트라넷",
remarks: "2024.05.22 인트라넷서버로 교체",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.72",
ip2: "192.168.10.7",
remoteAccess: [
{ tool: "Remote Util", id: "211.206.127.72", pw: "1234dkdlxl!" }
],
monitoring: "설치 X",
serverIdMatch: "",
model: "SYSTEM X3650 M2",
os: "Windows Server 2008 R2",
cpu: "Intel(R) Xeon(R) CPU E5520 @ 2.27GHz",
ram: "16GB",
storage: ["556GB"],
purchaseDate: ""
},
{
company: "PTC",
serverNo: "ptc-idc-002",
category: "예비서버",
remarks: "PTC 인트라넷 예비서버",
location: "서관 204번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "192.168.10.8",
ip2: "",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "1234dkdlxl!" }
],
monitoring: "O",
serverIdMatch: "",
model: "HPE ProLiant DL360 GEN10",
os: "Windows Server 2019",
cpu: "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz",
ram: "32GB",
storage: ["278GB", "1.09TB"],
purchaseDate: "2022.04.01"
},
{
company: "PTC",
serverNo: "ptc-idc-003",
category: "DB 백업 서버",
remarks: "2024.05.22 변경 (데스크탑)",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.74",
ip2: "192.168.10.9",
remoteAccess: [
{ tool: "Remote Util", id: "211.206.127.74", pw: "1234dkdlxl!" }
],
monitoring: "설치 X",
serverIdMatch: "",
model: "",
os: "Window 7",
cpu: "Intel(R) Core(TM)2 CPU 6400 @ 2.13GHz",
ram: "4GB",
storage: ["593GB", "1.23TB"],
purchaseDate: ""
},
{
company: "바론",
serverNo: "br-idc-001",
category: "인트라넷",
remarks: "",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "211.206.127.75",
ip2: "192.168.10.10",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "Hanmac2141!%" }
],
monitoring: "O",
serverIdMatch: "srcf0136042d",
model: "HPE ProLiant DL360 GEN10",
os: "Windows Server 2022",
cpu: "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz",
ram: "32GB",
storage: ["280GB", "2.18TB"],
purchaseDate: "2025.04.14"
},
{
company: "현타",
serverNo: "ht-idc-001",
category: "인트라넷",
remarks: "",
location: "동관 53번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip1: "1.234.37.172",
ip2: "192.168.20.172",
remoteAccess: [
{ tool: "원격데스크탑", id: "administrator", pw: "Hanmac2141!" }
],
monitoring: "O",
serverIdMatch: "src901e49933",
model: "HPE ProLiant DL380 GEN10",
os: "Windows Server 2019",
cpu: "Intel Xeon Silver 4210R CPU @ 2.40GHz",
ram: "32GB",
storage: ["280GB", "1TB"],
purchaseDate: "2022.09.21"
}
];
export const idcStorages: IdcStorage[] = [
{
company: "삼안",
serverNo: "sa-nas-001",
category: "인트라넷 백업 스토리지",
remarks: "",
location: "서관 203번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip: "118.220.172.246",
remoteAccess: [{ tool: "원격", id: "administrator", pw: "sg11707808" }],
model: "Promiss R Series",
capacity: "36TB",
purchaseDate: ""
},
{
company: "삼안",
serverNo: "sa-nas-002",
category: "성과품 스토리지",
remarks: "매니지먼트 접속 확인 불가",
location: "서관 205번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip: "118.220.172.248",
managementIp: "118.220.172.247",
remoteAccess: [{ tool: "원격", id: "administrator", pw: "sg11707808" }],
model: "ENC_3U_16BAY_D",
capacity: "23TB",
purchaseDate: "2019.06.03"
},
{
company: "삼안",
serverNo: "sa-nas-003",
category: "성과품 백업 스토리지",
remarks: "",
location: "서관 202번",
managerPrimary: "김철수",
managerSecondary: "홍길동",
ip: "118.220.172.241",
managementIp: "118.220.172.240",
remoteAccess: [
{ tool: "원격", id: "administrator", pw: "saman1!" },
{ tool: "원격", id: "admin0", pw: "Root1234" }
],
model: "Promiss R Series",
capacity: "48TB",
purchaseDate: "2025.03.13"
}
];

View File

@@ -0,0 +1,84 @@
export interface Category {
id: string;
name: string;
count: number;
}
export interface Asset {
id: string;
categoryName: string;
name: string;
quantity: number;
internalCode: string;
serialNumber: string;
department: string;
user: string;
acquisitionDate: string;
status: string;
location: string;
}
export interface HardwareSpec {
id: string;
pcName: string;
department: string;
userName: string;
os: string;
cpu: string;
memory: string;
disk: string;
macAddress: string;
ipAddress: string;
graphicCard: string;
}
export const mockCategories: Category[] = [
{ id: '1', name: 'PC', count: 1 },
{ id: '2', name: '모니터', count: 5 },
{ id: '3', name: '노트북', count: 2 },
];
export const mockAssets: Asset[] = [
{
id: '1',
categoryName: 'PC',
name: 'PC',
quantity: 1,
internalCode: 'PC20230411001',
serialNumber: 'SN-001-A1',
department: '한맥기술',
user: '이관형',
acquisitionDate: '2023-04-11',
status: '정상',
location: '본사 3층',
},
];
export const mockHardwareSpecs: HardwareSpec[] = [
{
id: '1',
pcName: 'DESKTOP-G1DVL26',
department: '한맥기술',
userName: '이준권',
os: 'Microsoft Windows 10 Pro 10.0.19044',
cpu: 'Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz',
memory: 'Samsung 4 GB / Samsung 4 GB',
disk: 'ST2000DM001-1CH164 1.82 TB / Samsung SSD 850 EVO 120GB',
macAddress: '0862664B98A3',
ipAddress: '172.16.9.68',
graphicCard: 'NVIDIA GeForce GTX 750',
},
{
id: '2',
pcName: 'DESKTOP-BNBPOUP',
department: '한맥기술',
userName: '주완기 연구원',
os: 'Microsoft Windows 10 Pro 10.0.19045',
cpu: 'Intel(R) Core(TM) i5-4570 CPU @ 3.20GHz',
memory: 'Samsung 8 GB / Samsung 8 GB',
disk: 'ST1000DM003-1CH162 931.51 GB / Samsung SSD 840 EVO 120GB',
macAddress: 'E03F4948ECC6',
ipAddress: '172.16.9.23',
graphicCard: 'Intel(R) HD Graphics 4600',
},
];

26
backup_temp/src/index.css Normal file
View File

@@ -0,0 +1,26 @@
:root {
font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
line-height: 1.5;
font-weight: 400;
letter-spacing: -0.02em;
color-scheme: light;
color: #111827;
background-color: #F9FAFB;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
display: flex;
min-width: 320px;
min-height: 100vh;
}
#root {
width: 100%;
}

10
backup_temp/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,49 @@
IDC,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
서버목록,,,,,,,,,,,,,,,,,,,,,,,,,,한맥,LMS 서버,mdf,
회사,서버번호,구분,비고,설치위치,담당자,,IP 주소,,원격접속,,,모니터링 여부,"서버 이름 ID
일치 여부, 전 서버 이름",제조사 및 모델명,OS,CPU,RAM,Storage1,Storage2,Storage3,계산서,,,,,삼안,XR플래그십 스토리지,,20250414
,,,,,,,IP,IP2,접속도구,아이디,비빌번호,,,,,,,,,,,,,,,장헌,"스토리지, 서버",mdf 추정,
한맥,hm-idc-001,한맥 인트라넷,,서관 204번,,,211.206.127.70,192.168.10.5,원격데스크탑,administrator,samanerp1!,"win exp, raid X",srv07d330084,HPE ProLiant DL360 Gen10,Windows Server 2016,intel xeon silver4110 CPU @2.10GHz 2.10GHZ,32GB,280GB,2.7TB,,20201210 추정,한맥,,,,한라,백업서버,mdf,
,,,,,,,,,Remote Util,211.206.127.70,1234아이티!,,,,,,,,,,,,,,,,,,
,hm-idc-002,한맥 인트라넷 예비,"단가, 입사자지원 서버 (4/1 장헌산업 이름으로 스마트 건설 용도 구매)",서관 205번,,,211.206.127.78,192.168.10.13,원격데스크탑,administrator,Hanmac2141!,"win exp, raid X",srcff5294c84,HPE ProLiant DL360 Gen10,Windows Server 2019,intel xeon silver4214R CPU @2.40GHz 2.39GHZ,32GB,280GB,2.7TB,,,,,,,,,,
삼안,sa-idc-001,삼안 인트라넷,,서관 204번,,,118.220.172.237,erp.samaneng.com,원격데스크탑,administrator,samanerp1!,O,newSmintranet,HPE ProLiant DL360 Gen10,Windows Server 2016,intel xeon silver4214R CPU @2.40GHz 2.39GHZ,32GB,280GB,3.27TB,,20191220 추정,삼안,,,,,,,
,,,,,,,,,Remote Util,118.220.172.237,1234아이티!,,,,,,,,,,,,,,,,,,
,sa-idc-002,삼안 인트라넷 예비,,서관 204번,,,118.220.172.249,,원격데스크탑,administrator,samanerp1!,설치 X,INTRANET,HPE ProLiant DL360 GEN9,Windows Server 2008 R2,Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz 2.40GHz,32GB,279GB,2.72TB,,,,,,,,,,
,,,,,,,,,Remote Util,678-605-383-130,1234아이티!,,,,,,,,,,,,,,,,,,
,sa-idc-003,SATIS 01,"구 SATIS 서버, 세금계산서 발행(회계)",서관 204번,,,118.220.172.228,,원격데스크탑,administrator,satissg11707808,설치 X,satis01,HPE ProLiant DL380p GEN8,Windows Server 2008 R2,Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz 3.30GHz,20GB,100GB,458GB,,,,,,,,,,
,sa-idc-004,SATIS 02,SATIS 리뉴얼 버전 (ERP 서버),서관 204번,,,118.220.172.229,,원격데스크탑,administrator,satissg11707808,설치 X,satis02,HPE ProLiant DL380p GEN8,Windows Server 2008 R2,Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz 3.30GHz,20GB,100GB,458GB,18.1TB,,,,,,,,,
,sa-idc-005,웹 서버,남양주 테스트 서버 (도메인 관리 기능 제거 2026.03.11),서관 204번,,,samanweb.cafe24.com,118.220.172.195,원격데스크탑,administrator,saman+2013+web,"win exp, 포트 안열림",www,HPE ProLiant DL380p GEN8,Windwos Server 2012,Intel(R) Xeon(R) CPU E5-2609 0 @ 2.40GHz 2.40GHz,16GB,100GB,230GB,230GB,,,,,,,,,
,sa-idc-006,PQ DB 서버,,서관 204번,,,118.220.172.231,,원격데스크탑,administrator,7013ddj10235!, O,src5dd67f2ed,HPE ProLiant DL360 Gen10,Windows Server 2019,intel xeon silver4210R CPU @2.40GHz 2.39GHZ,32GB,278GB,2.18TB,,20241216 구매교체,삼안,,,,,,,
,sa-idc-007,Oracle DB 서버,,서관 202번,,,118.220.172.225,,원격데스크탑,administrator,7013ddj10235!,"win exp, raid X",SAMAN-DB,HPE ProLiant DL380 GEN9,Windows Server 2012,Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz 2.20GHz,64GB,558GB,1.09TB,1.09TB,,,,,,,,,
,sa-idc-008,안전관리,"삼안 개발서버2 - AI, SSL, 장헌TBM, 노드",서관 202번,,,1.234.37.171,,원격데스크탑,administrator,samanerp1!,연결 X,,HPE ProLiant DL380 GEN10,Windwos Server 2022,Intel Xeon(R) Silver 4210R CPU @ 2.40GHz,128GB,278GB,3.27TB,,20250410 설치,삼안,,,,,,,
,sa-idc-009,가족사 공통메뉴,"삼안 개발서버1 - QNA, 급여명세서",서관 202번,,,118.220.172.233,,원격데스크탑,administrator,samanerp1!,O,srcc9ac928ee,HPE ProLiant DL380 GEN10,Windwos Server 2022,Intel Xeon(R) Silver 4210R CPU @ 2.40GHz,128GB,278GB,3.27TB,,20250410 설치,삼안,,,,,,,
한라,hl-idc-001,한라 인트라넷,"인트라넷,안전, 운영, MISO 서버로 운영 중(win 2008)",동관 54번,,,1.234.37.143,,Remote Util,1.234.37.143,1234dkdlxl!,설치 X,,HPE ProLiant DL360 GEN9,Windows Server 2008 R2,Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz 1.70GHz,8GB,299GB,631GB,,,,,,,,,,
,hl-idc-002,안전전산화 서버 (디자인팀 웹),"인트라넷 서버 다운 시 백업용 대기, (임시) 디자인팀 웹 퍼블리싱 서버",동관 54번,,,1.234.37.144,192.168.20.49,Remote Util,1.234.37.144,1234dkdlxl!,O,,HPE ProLiant DL360 GEN9,Windows Server 2012,Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz 1.70GHz,8GB,299GB,631GB,,,,,,,,,,
,hl-idc-003,개발서버2,PTC 연구비로 구매한 예비서버2 ,동관 53번,,,192.168.20.171,1.234.37.171,Remote Util,1.234.37.171,1234dkdlxl!,O,,HPE ProLiant DL380 Gen10,Windows Server 2019 Standard,Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz,32GB,280GB,1TB,,20220921 추정,ptc,,,,,,,
,,,"이전 : 하수도자산 소스+프로그램 현재 : 큰길 서비스용 xampp+ PostgreSQL, BEPs",,,,,,원격데스크탑,administrator,Hanmac2141!%,,src775d3e5df,,,,,,,,,,,,,,,,
장헌,jh-idc-001,장헌인트라넷,,서관 205번,,,211.206.127.71,192.168.10.6,Remote Util,211.206.127.71,1234dkdlxl!,잠금 걸려있음,,HPE ProLiant DL380 GEN10,Windows Server 2019,Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 2.39GHz,32GB,280GB,1TB,,20220921 추정,ptc,,,,,,,
,jh-idc-002,장헌 인트라넷 예비,,동관 53번,,,1.234.37.170,192.168.20.170,Remote Util,1.234.37.170,1234dkdlxl!,"원격 X, O
",,HPE ProLiant DL360 Gen10,Windows Server 2019,Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 2.39GHz,32GB,280GB,1TB,,20220401 추정,장헌,,,,,,,
,,,,,,,,,원격데스크탑,Administrator,Hanmac2141!,,,,,,,,,,,,,,,,,,
,jh-idc-003,인트라넷(구),현재는 GIT 백업 으로 사용,서관 205번,,,211.206.127.110,192.168.10.40,Remote Util,211.206.127.110,1234dkdlxl!,,,,Windows Server 2019,Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz,,,,,,,,,,,,,
,,,,,,,,,원격데스크탑,User,Hanmac2141!,,,,,,,,,,,,,,,,,,
(주)장헌,jh-idc-004,(주) 장헌 인트라넷,2025.12.23 (주) 장헌 센터 MDF에서 IDC로 이전 설치,서관 205번,,,211.206.127.76,,원격데스크탑,User,Hanmac2141!%,"win exp, raid X",DESKTOP-5IL75B7,,Windows 10,12th Gen Intel(R) Core(TM) i7-12700F,32GB,465GB,1.81TB,,,,,,,,,,
PTC,ptc-idc-001,PTC인트라넷,"구 파일 서버(부서자료 백업용), 2024.05.22 인트라넷서버로 교체",서관 205번,,,211.206.127.72,192.168.10.7,Remote Util,211.206.127.72,1234dkdlxl!,설치 X,,SYSTEM X3650 M2,Windows Server 2008 R2,Intel(R) Xeon(R) CPU E5520 @ 2.27GHz 2.26GHz,16GB,556GB,,,,,,,,,,,
,ptc-idc-002,예비서버,PTC 인트라넷 예비서버,서관 204번,,,192.168.10.8,,원격데스크탑,administrator,1234dkdlxl!,O,,HPE ProLiant DL360 GEN10,Windows Server 2019,Intel Xeon(R) Silver 4210R CPU @ 2.40GHz,32GB,278GB,1.09TB,,20220401 추정,장헌,,,,,,,
,ptc-idc-003,DB 백업 서버,"구 파일 인트라넷, 2024.05.22에 DB 백업 테스트 서버로 변경 (데스크탑)",서관 205번,,,211.206.127.74,192.168.10.9,Remote Util,211.206.127.74,1234dkdlxl!,설치 X,,,Window 7,Intel(R) Core(TM)2 CPU 6400 @ 2.13GHz 2.13GHz,4GB,593GB,1.23TB,,,,,,,,,,
바론,br-idc-001,인트라넷,,서관 205번,,,211.206.127.75,192.168.10.10,원격데스크탑,administrator,Hanmac2141!%,O,srcf0136042d,HPE ProLiant DL360 GEN10,Windows Server 2022,Intel Xeon(R) Silver 4210R CPU @ 2.40GHz,32GB,280GB,2.18TB,,20250414 추정,바론,,,,,,,
현타,ht-idc-001,인트라넷,,동관 53번,,,1.234.37.172,192.168.20.172,원격데스크탑,administrator,Hanmac2141!,O,src901e49933,HPE ProLiant DL380 GEN10,Windows Server 2019,Intel Xeon Silver 4210R CPU @ 2.40GHz 2.39GHz,32GB,280GB,1TB,,20220921 추정,ptc,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
스토리지 목록,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
회사,서버번호,구분,비고,설치위치,담당자,,IP 주소,,원격접속,,,,,,모델명,용량,,,,,,,,,,,,,
,,,,,,,,,접속도구,아이디,비빌번호,,,,,,,,,,,,,,,,,,
삼안,sa-das-001,,"Satis01, Satis02 광케이블 연결 (물리연결)",서관 205번,,,,,,,,,,,,,,,,,,,,,,,,,
,sa-nas-001,인트라넷 백업 스토리지,,서관 203번,,,(IDC) 118.220.172.246,,원격,administrator,sg11707808,,,,Promiss R Series,36TB,,,,,,,,,,,,,
,sa-nas-002,성과품 스토리지,,서관 205번,,,(IDC) 118.220.172.248,,원격,administrator,sg11707808,,,,ENC_3U_16BAY_D // SEAGATE ST2000NM0045,23TB,,,,,20190603 추정,삼안,,,,,,,
,,,매니지먼트 접속 확인 불가 (콘솔 연결 후 페이지 오픈 필요),,,,(매니지먼트) 118.220.172.247,,원격,-,-,,,,,,,,,,,,,,,,,,
,sa-nas-003,성과품 백업 스토리지,,서관 202번,,,(IDC) 118.220.172.241,,원격,administrator,saman1!,,,,Promiss R Series,48TB,,,,,20250313,삼안,,,,,,,
,,,,,,,(매지니먼트) 118.220.172.240,,원격,admin0,Root1234,,,,,,,,,,,,,,,,,,
한라,hl-das-001,,파일서버 정보 없음(접속 불가),동관 54번,,,,,,,,,,,,,,,,,20190701 추정,한라,(한라 파일서버도 동일일자 추정),,,,,,
,hl-das-002,,,동관 54번,,,,,,,,,,,,,,,,,,,,,,,,,
1 IDC
2 서버목록 한맥 LMS 서버 mdf
3 회사 서버번호 구분 비고 설치위치 담당자 IP 주소 원격접속 모니터링 여부 서버 이름 ID 일치 여부, 전 서버 이름 제조사 및 모델명 OS CPU RAM Storage1 Storage2 Storage3 계산서 삼안 XR플래그십 스토리지 20250414
4 IP IP2 접속도구 아이디 비빌번호 장헌 스토리지, 서버 mdf 추정
5 한맥 hm-idc-001 한맥 인트라넷 서관 204번 211.206.127.70 192.168.10.5 원격데스크탑 administrator samanerp1! win exp, raid X srv07d330084 HPE ProLiant DL360 Gen10 Windows Server 2016 intel xeon silver4110 CPU @2.10GHz 2.10GHZ 32GB 280GB 2.7TB 20201210 추정 한맥 한라 백업서버 mdf
6 Remote Util 211.206.127.70 1234아이티!
7 hm-idc-002 한맥 인트라넷 예비 단가, 입사자지원 서버 (4/1 장헌산업 이름으로 스마트 건설 용도 구매) 서관 205번 211.206.127.78 192.168.10.13 원격데스크탑 administrator Hanmac2141! win exp, raid X srcff5294c84 HPE ProLiant DL360 Gen10 Windows Server 2019 intel xeon silver4214R CPU @2.40GHz 2.39GHZ 32GB 280GB 2.7TB
8 삼안 sa-idc-001 삼안 인트라넷 서관 204번 118.220.172.237 erp.samaneng.com 원격데스크탑 administrator samanerp1! O newSmintranet HPE ProLiant DL360 Gen10 Windows Server 2016 intel xeon silver4214R CPU @2.40GHz 2.39GHZ 32GB 280GB 3.27TB 20191220 추정 삼안
9 Remote Util 118.220.172.237 1234아이티!
10 sa-idc-002 삼안 인트라넷 예비 서관 204번 118.220.172.249 원격데스크탑 administrator samanerp1! 설치 X INTRANET HPE ProLiant DL360 GEN9 Windows Server 2008 R2 Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz 2.40GHz 32GB 279GB 2.72TB
11 Remote Util 678-605-383-130 1234아이티!
12 sa-idc-003 SATIS 01 구 SATIS 서버, 세금계산서 발행(회계) 서관 204번 118.220.172.228 원격데스크탑 administrator satissg11707808 설치 X satis01 HPE ProLiant DL380p GEN8 Windows Server 2008 R2 Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz 3.30GHz 20GB 100GB 458GB
13 sa-idc-004 SATIS 02 SATIS 리뉴얼 버전 (ERP 서버) 서관 204번 118.220.172.229 원격데스크탑 administrator satissg11707808 설치 X satis02 HPE ProLiant DL380p GEN8 Windows Server 2008 R2 Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz 3.30GHz 20GB 100GB 458GB 18.1TB
14 sa-idc-005 웹 서버 남양주 테스트 서버 (도메인 관리 기능 제거 2026.03.11) 서관 204번 samanweb.cafe24.com 118.220.172.195 원격데스크탑 administrator saman+2013+web win exp, 포트 안열림 www HPE ProLiant DL380p GEN8 Windwos Server 2012 Intel(R) Xeon(R) CPU E5-2609 0 @ 2.40GHz 2.40GHz 16GB 100GB 230GB 230GB
15 sa-idc-006 PQ DB 서버 서관 204번 118.220.172.231 원격데스크탑 administrator 7013ddj10235! O src5dd67f2ed HPE ProLiant DL360 Gen10 Windows Server 2019 intel xeon silver4210R CPU @2.40GHz 2.39GHZ 32GB 278GB 2.18TB 20241216 구매교체 삼안
16 sa-idc-007 Oracle DB 서버 서관 202번 118.220.172.225 원격데스크탑 administrator 7013ddj10235! win exp, raid X SAMAN-DB HPE ProLiant DL380 GEN9 Windows Server 2012 Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz 2.20GHz 64GB 558GB 1.09TB 1.09TB
17 sa-idc-008 안전관리 삼안 개발서버2 - AI, SSL, 장헌TBM, 노드 서관 202번 1.234.37.171 원격데스크탑 administrator samanerp1! 연결 X HPE ProLiant DL380 GEN10 Windwos Server 2022 Intel Xeon(R) Silver 4210R CPU @ 2.40GHz 128GB 278GB 3.27TB 20250410 설치 삼안
18 sa-idc-009 가족사 공통메뉴 삼안 개발서버1 - QNA, 급여명세서 서관 202번 118.220.172.233 원격데스크탑 administrator samanerp1! O srcc9ac928ee HPE ProLiant DL380 GEN10 Windwos Server 2022 Intel Xeon(R) Silver 4210R CPU @ 2.40GHz 128GB 278GB 3.27TB 20250410 설치 삼안
19 한라 hl-idc-001 한라 인트라넷 인트라넷,안전, 운영, MISO 서버로 운영 중(win 2008) 동관 54번 1.234.37.143 Remote Util 1.234.37.143 1234dkdlxl! 설치 X HPE ProLiant DL360 GEN9 Windows Server 2008 R2 Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz 1.70GHz 8GB 299GB 631GB
20 hl-idc-002 안전전산화 서버 (디자인팀 웹) 인트라넷 서버 다운 시 백업용 대기, (임시) 디자인팀 웹 퍼블리싱 서버 동관 54번 1.234.37.144 192.168.20.49 Remote Util 1.234.37.144 1234dkdlxl! O HPE ProLiant DL360 GEN9 Windows Server 2012 Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz 1.70GHz 8GB 299GB 631GB
21 hl-idc-003 개발서버2 PTC 연구비로 구매한 예비서버2 동관 53번 192.168.20.171 1.234.37.171 Remote Util 1.234.37.171 1234dkdlxl! O HPE ProLiant DL380 Gen10 Windows Server 2019 Standard Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 32GB 280GB 1TB 20220921 추정 ptc
22 이전 : 하수도자산 소스+프로그램 현재 : 큰길 서비스용 xampp+ PostgreSQL, BEPs 원격데스크탑 administrator Hanmac2141!% src775d3e5df
23 장헌 jh-idc-001 장헌인트라넷 서관 205번 211.206.127.71 192.168.10.6 Remote Util 211.206.127.71 1234dkdlxl! 잠금 걸려있음 HPE ProLiant DL380 GEN10 Windows Server 2019 Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 2.39GHz 32GB 280GB 1TB 20220921 추정 ptc
24 jh-idc-002 장헌 인트라넷 예비 동관 53번 1.234.37.170 192.168.20.170 Remote Util 1.234.37.170 1234dkdlxl! 원격 X, O HPE ProLiant DL360 Gen10 Windows Server 2019 Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 2.39GHz 32GB 280GB 1TB 20220401 추정 장헌
25 원격데스크탑 Administrator Hanmac2141!
26 jh-idc-003 인트라넷(구) 현재는 GIT 백업 으로 사용 서관 205번 211.206.127.110 192.168.10.40 Remote Util 211.206.127.110 1234dkdlxl! Windows Server 2019 Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz
27 원격데스크탑 User Hanmac2141!
28 (주)장헌 jh-idc-004 (주) 장헌 인트라넷 2025.12.23 (주) 장헌 센터 MDF에서 IDC로 이전 설치 서관 205번 211.206.127.76 원격데스크탑 User Hanmac2141!% win exp, raid X DESKTOP-5IL75B7 Windows 10 12th Gen Intel(R) Core(TM) i7-12700F 32GB 465GB 1.81TB
29 PTC ptc-idc-001 PTC인트라넷 구 파일 서버(부서자료 백업용), 2024.05.22 인트라넷서버로 교체 서관 205번 211.206.127.72 192.168.10.7 Remote Util 211.206.127.72 1234dkdlxl! 설치 X SYSTEM X3650 M2 Windows Server 2008 R2 Intel(R) Xeon(R) CPU E5520 @ 2.27GHz 2.26GHz 16GB 556GB
30 ptc-idc-002 예비서버 PTC 인트라넷 예비서버 서관 204번 192.168.10.8 원격데스크탑 administrator 1234dkdlxl! O HPE ProLiant DL360 GEN10 Windows Server 2019 Intel Xeon(R) Silver 4210R CPU @ 2.40GHz 32GB 278GB 1.09TB 20220401 추정 장헌
31 ptc-idc-003 DB 백업 서버 구 파일 인트라넷, 2024.05.22에 DB 백업 테스트 서버로 변경 (데스크탑) 서관 205번 211.206.127.74 192.168.10.9 Remote Util 211.206.127.74 1234dkdlxl! 설치 X Window 7 Intel(R) Core(TM)2 CPU 6400 @ 2.13GHz 2.13GHz 4GB 593GB 1.23TB
32 바론 br-idc-001 인트라넷 서관 205번 211.206.127.75 192.168.10.10 원격데스크탑 administrator Hanmac2141!% O srcf0136042d HPE ProLiant DL360 GEN10 Windows Server 2022 Intel Xeon(R) Silver 4210R CPU @ 2.40GHz 32GB 280GB 2.18TB 20250414 추정 바론
33 현타 ht-idc-001 인트라넷 동관 53번 1.234.37.172 192.168.20.172 원격데스크탑 administrator Hanmac2141! O src901e49933 HPE ProLiant DL380 GEN10 Windows Server 2019 Intel Xeon Silver 4210R CPU @ 2.40GHz 2.39GHz 32GB 280GB 1TB 20220921 추정 ptc
34
35
36
37 스토리지 목록
38 회사 서버번호 구분 비고 설치위치 담당자 IP 주소 원격접속 모델명 용량
39 접속도구 아이디 비빌번호
40 삼안 sa-das-001 Satis01, Satis02 광케이블 연결 (물리연결) 서관 205번
41 sa-nas-001 인트라넷 백업 스토리지 서관 203번 (IDC) 118.220.172.246 원격 administrator sg11707808 Promiss R Series 36TB
42 sa-nas-002 성과품 스토리지 서관 205번 (IDC) 118.220.172.248 원격 administrator sg11707808 ENC_3U_16BAY_D // SEAGATE ST2000NM0045 23TB 20190603 추정 삼안
43 매니지먼트 접속 확인 불가 (콘솔 연결 후 페이지 오픈 필요) (매니지먼트) 118.220.172.247 원격 - -
44 sa-nas-003 성과품 백업 스토리지 서관 202번 (IDC) 118.220.172.241 원격 administrator saman1! Promiss R Series 48TB 20250313 삼안
45 (매지니먼트) 118.220.172.240 원격 admin0 Root1234
46 한라 hl-das-001 파일서버 정보 없음(접속 불가) 동관 54번 20190701 추정 한라 (한라 파일서버도 동일일자 추정)
47 hl-das-002 동관 54번

Binary file not shown.

39
package-lock.json generated
View File

@@ -499,9 +499,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -516,9 +513,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -533,9 +527,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -550,9 +541,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -567,9 +555,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -584,9 +569,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -601,9 +583,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -618,9 +597,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -635,9 +611,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -652,9 +625,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -669,9 +639,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -686,9 +653,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -703,9 +667,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [

View File

@@ -0,0 +1,46 @@
/**
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다.
*/
export function initBaseModal() {
const modals = document.querySelectorAll('.modal-overlay');
const closeButtons = document.querySelectorAll('.btn-icon, [id^="btn-cancel-"]');
// 모든 모달 닫기 함수
const closeAllModals = () => {
modals.forEach(modal => modal.classList.add('hidden'));
// SW 관련 추가 모달 처리
document.getElementById('sw-user-modal')?.classList.add('hidden');
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
document.getElementById('dashboard-detail-modal')?.classList.add('hidden');
};
// 닫기 버튼 이벤트 바인딩
closeButtons.forEach(btn => {
btn.addEventListener('click', closeAllModals);
});
// ESC 키로 닫기
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeAllModals();
});
// 배경(Overlay) 클릭 시 닫기
modals.forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) closeAllModals();
});
});
return { closeAllModals };
}
/**
* 특정 모달을 엽니다.
* @param modalId 모달 엘리먼트의 ID
*/
export function openModal(modalId: string) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('hidden');
}
}

View File

@@ -0,0 +1,112 @@
import { state } from '../../state';
import { HardwareAsset } from '../../excelHandler';
import { openModal } from './BaseModal';
/**
* 하드웨어(서버, 전산비품 등) 모달 초기화 및 로직 제어
*/
export function initHWModal(renderContent: () => void, closeModals: () => void) {
const hwForm = document.getElementById('hw-asset-form') as HTMLFormElement;
const btnSaveHw = document.getElementById('btn-save-hw-asset') as HTMLButtonElement;
const btnDeleteHw = document.getElementById('btn-delete-hw-asset') as HTMLButtonElement;
// 저장 버튼 이벤트
btnSaveHw?.addEventListener('click', (e) => {
e.preventDefault();
if (!hwForm.checkValidity()) { hwForm.reportValidity(); return; }
const id = (document.getElementById('hw-asset-id') as HTMLInputElement).value;
const fileInput = document.getElementById('hw-품의서') as HTMLInputElement;
const = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('hw-품의서명') as HTMLElement).innerText.replace('📎', '');
const newAsset: HardwareAsset = {
id: id || Math.random().toString(36).substring(2, 9),
type: (document.getElementById('hw-asset-type') as HTMLInputElement).value,
: (document.getElementById('hw-법인') as HTMLInputElement).value,
: (document.getElementById('hw-자산코드') as HTMLInputElement).value,
: (document.getElementById('hw-명칭') as HTMLInputElement).value,
: (document.getElementById('hw-위치') as HTMLInputElement).value,
: (document.getElementById('hw-관리자') as HTMLInputElement).value,
IP주소: (document.getElementById('hw-IP주소') as HTMLInputElement).value,
MACaddress: (document.getElementById('hw-MACaddress') as HTMLInputElement).value,
OS: (document.getElementById('hw-OS') as HTMLInputElement).value,
HW사양: (document.getElementById('hw-HW사양') as HTMLTextAreaElement).value,
: (document.getElementById('hw-구매일') as HTMLInputElement).value,
: (document.getElementById('hw-금액') as HTMLInputElement).value,
: (document.getElementById('hw-납품업체') as HTMLInputElement).value,
,
: (document.getElementById('hw-asset-type') as HTMLInputElement).value === '전산비품'
? (document.getElementById('hw-비품유형') as HTMLSelectElement).value : undefined
};
if (id) {
const idx = state.masterData.hw.findIndex(a => a.id === id);
if(idx !== -1) state.masterData.hw[idx] = newAsset;
} else {
state.masterData.hw.push(newAsset);
}
closeModals();
renderContent();
});
// 삭제 버튼 이벤트
btnDeleteHw?.addEventListener('click', (e) => {
e.preventDefault();
const id = (document.getElementById('hw-asset-id') as HTMLInputElement).value;
if (confirm('삭제하시겠습니까?')) {
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
closeModals();
renderContent();
}
});
}
/**
* 하드웨어 상세 모달 열기
* @param asset 수정 시 자산 데이터, 신규 시 undefined
*/
export function openHwModal(asset?: HardwareAsset) {
const hwModal = document.getElementById('hw-asset-modal') as HTMLDivElement;
const hwForm = document.getElementById('hw-asset-form') as HTMLFormElement;
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
openModal('hw-asset-modal');
hwForm.reset();
if (asset) {
document.getElementById('hw-modal-title')!.textContent = '자산 상세 정보 수정';
deleteBtn.style.display = 'block';
(document.getElementById('hw-asset-id') as HTMLInputElement).value = asset.id;
(document.getElementById('hw-asset-type') as HTMLInputElement).value = asset.type;
(document.getElementById('hw-법인') as HTMLInputElement).value = asset.;
(document.getElementById('hw-자산코드') as HTMLInputElement).value = asset.;
(document.getElementById('hw-명칭') as HTMLInputElement).value = asset.;
(document.getElementById('hw-위치') as HTMLInputElement).value = asset.;
(document.getElementById('hw-관리자') as HTMLInputElement).value = asset.;
(document.getElementById('hw-IP주소') as HTMLInputElement).value = asset.IP주소;
(document.getElementById('hw-MACaddress') as HTMLInputElement).value = asset.MACaddress;
(document.getElementById('hw-OS') as HTMLInputElement).value = asset.OS;
(document.getElementById('hw-HW사양') as HTMLTextAreaElement).value = asset.HW사양;
(document.getElementById('hw-구매일') as HTMLInputElement).value = asset. || '';
(document.getElementById('hw-금액') as HTMLInputElement).value = asset. ? Number(asset..replace(/,/g, '')).toLocaleString() : '';
(document.getElementById('hw-납품업체') as HTMLInputElement).value = asset. || '';
(document.getElementById('hw-품의서명') as HTMLElement).innerText = asset. ? `📎${asset.}` : '';
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = asset. || '노트북';
} else {
document.getElementById('hw-modal-title')!.textContent = `${state.activeSubTab} 자산 추가`;
deleteBtn.style.display = 'none';
(document.getElementById('hw-asset-id') as HTMLInputElement).value = '';
(document.getElementById('hw-asset-type') as HTMLInputElement).value = state.activeSubTab;
(document.getElementById('hw-품의서명') as HTMLElement).innerText = '';
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = '노트북';
}
// 전산비품일 경우 유형 선택 필드 노출
if (state.activeSubTab === '전산비품') {
document.getElementById('hw-비품유형-group')!.style.display = 'block';
} else {
document.getElementById('hw-비품유형-group')!.style.display = 'none';
}
}

View File

@@ -0,0 +1,111 @@
import { state } from '../../state';
import { HardwareAsset } from '../../excelHandler';
import { openModal } from './BaseModal';
/**
* 개인PC 모달 초기화 및 로직 제어
*/
export function initPCModal(renderContent: () => void, closeModals: () => void) {
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
// 저장 버튼 이벤트
btnSavePc?.addEventListener('click', (e) => {
e.preventDefault();
if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; }
const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value;
const fileInput = document.getElementById('pc-품의서') as HTMLInputElement;
const = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('pc-품의서명') as HTMLElement).innerText.replace('📎', '');
const newAsset: HardwareAsset = {
id: id || Math.random().toString(36).substring(2, 9),
type: '개인PC',
: (document.getElementById('pc-법인') as HTMLSelectElement).value,
: (document.getElementById('pc-자산코드') as HTMLInputElement).value,
: '',
: (document.getElementById('pc-위치') as HTMLInputElement).value,
: '',
IP주소: '',
MACaddress: '',
HW사양: '',
OS: '',
: (document.getElementById('pc-사용자') as HTMLInputElement).value,
CPU: (document.getElementById('pc-CPU') as HTMLInputElement).value,
GPU: (document.getElementById('pc-GPU') as HTMLInputElement).value,
RAM: (document.getElementById('pc-RAM') as HTMLInputElement).value,
SSD1: (document.getElementById('pc-SSD1') as HTMLInputElement).value,
SSD2: (document.getElementById('pc-SSD2') as HTMLInputElement).value,
HDD1: (document.getElementById('pc-HDD1') as HTMLInputElement).value,
HDD2: (document.getElementById('pc-HDD2') as HTMLInputElement).value,
: (document.getElementById('pc-구매일') as HTMLInputElement).value,
: (document.getElementById('pc-금액') as HTMLInputElement).value,
: (document.getElementById('pc-납품업체') as HTMLInputElement).value,
};
if (id) {
const idx = state.masterData.hw.findIndex(a => a.id === id);
if(idx !== -1) state.masterData.hw[idx] = newAsset;
} else {
state.masterData.hw.push(newAsset);
}
closeModals();
renderContent();
});
// 삭제 버튼 이벤트
btnDeletePc?.addEventListener('click', (e) => {
e.preventDefault();
const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value;
if (confirm('삭제하시겠습니까?')) {
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
closeModals();
renderContent();
}
});
}
/**
* 개인PC 상세 모달 열기
* @param asset 수정 시 자산 데이터, 신규 시 undefined
*/
export function openPcModal(asset?: HardwareAsset) {
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
const deleteBtn = document.getElementById('btn-delete-pc-asset')!;
openModal('pc-asset-modal');
pcForm.reset();
if (asset) {
document.getElementById('pc-modal-title')!.textContent = '개인PC 상세 정보 수정';
deleteBtn.style.display = 'block';
(document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id;
(document.getElementById('pc-법인') as HTMLSelectElement).value = asset.;
(document.getElementById('pc-자산코드') as HTMLInputElement).value = asset.;
(document.getElementById('pc-사용자') as HTMLInputElement).value = asset. || '';
(document.getElementById('pc-위치') as HTMLInputElement).value = asset. || '';
(document.getElementById('pc-CPU') as HTMLInputElement).value = asset.CPU || '';
(document.getElementById('pc-GPU') as HTMLInputElement).value = asset.GPU || '';
(document.getElementById('pc-RAM') as HTMLInputElement).value = asset.RAM || '';
(document.getElementById('pc-SSD1') as HTMLInputElement).value = asset.SSD1 || '';
(document.getElementById('pc-SSD2') as HTMLInputElement).value = asset.SSD2 || '';
(document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || '';
(document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || '';
(document.getElementById('pc-구매일') as HTMLInputElement).value = asset. || '';
(document.getElementById('pc-금액') as HTMLInputElement).value = asset. ? Number(asset..replace(/,/g, '')).toLocaleString() : '';
(document.getElementById('pc-납품업체') as HTMLInputElement).value = asset. || '';
(document.getElementById('pc-품의서명') as HTMLElement).innerText = asset. ? `📎${asset.}` : '';
} else {
document.getElementById('pc-modal-title')!.textContent = '새 개인PC 자산 추가';
deleteBtn.style.display = 'none';
(document.getElementById('pc-asset-id') as HTMLInputElement).value = '';
(document.getElementById('pc-법인') as HTMLSelectElement).value = '한맥';
(document.getElementById('pc-품의서명') as HTMLElement).innerText = '';
}
}

View File

@@ -0,0 +1,102 @@
import { state } from '../../state';
import { SoftwareAsset } from '../../excelHandler';
import { openModal } from './BaseModal';
/**
* 소프트웨어 모달 초기화 및 로직 제어
*/
export function initSWModal(renderContent: () => void, closeModals: () => void) {
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement;
const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement;
// 저장 버튼 이벤트
btnSaveSw?.addEventListener('click', (e) => {
e.preventDefault();
if (!swForm.checkValidity()) { swForm.reportValidity(); return; }
const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value;
const newAsset: SoftwareAsset = {
id: id || Math.random().toString(36).substring(2, 9),
type: (document.getElementById('sw-asset-type') as HTMLInputElement).value,
: (document.getElementById('sw-법인') as HTMLSelectElement).value,
: (document.getElementById('sw-제품명') as HTMLInputElement).value,
: (document.getElementById('sw-구매일') as HTMLInputElement).value,
: (document.getElementById('sw-구독일') as HTMLInputElement).value,
: (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked,
: (document.getElementById('sw-금액') as HTMLInputElement).value,
수량: parseInt((document.getElementById('sw-수량') as HTMLInputElement).value || '1', 10),
: (document.getElementById('sw-계정명') as HTMLInputElement).value,
: (document.getElementById('sw-납품업체') as HTMLInputElement).value,
: (document.getElementById('sw-비고') as HTMLInputElement).value,
};
if (id) {
const idx = state.masterData.sw.findIndex(a => a.id === id);
if(idx !== -1) state.masterData.sw[idx] = newAsset;
} else {
state.masterData.sw.push(newAsset);
}
closeModals();
renderContent();
});
// 삭제 버튼 이벤트
btnDeleteSw?.addEventListener('click', (e) => {
e.preventDefault();
const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value;
if (confirm('삭제하시겠습니까?')) {
state.masterData.sw = state.masterData.sw.filter(a => a.id !== id);
closeModals();
renderContent();
}
});
}
/**
* 소프트웨어 상세 모달 열기
* @param asset 수정 시 자산 데이터, 신규 시 undefined
*/
export function openSwModal(asset?: SoftwareAsset) {
const swModal = document.getElementById('sw-asset-modal') as HTMLDivElement;
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
openModal('sw-asset-modal');
swForm.reset();
const subGroup = document.getElementById('sw-구독일-group')!;
const permGroup = document.getElementById('sw-유지보수-group')!;
if (state.activeSubTab === '구독SW') {
subGroup.style.display = 'block';
permGroup.style.display = 'none';
} else {
subGroup.style.display = 'none';
permGroup.style.display = 'block';
}
if (asset) {
document.getElementById('sw-modal-title')!.textContent = `${state.activeSubTab} 상세 정보 수정`;
deleteBtn.style.display = 'block';
(document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id;
(document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type;
(document.getElementById('sw-법인') as HTMLSelectElement).value = asset.;
(document.getElementById('sw-제품명') as HTMLInputElement).value = asset.;
(document.getElementById('sw-구매일') as HTMLInputElement).value = asset. || '';
(document.getElementById('sw-구독일') as HTMLInputElement).value = asset. || '';
(document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = !!asset.;
(document.getElementById('sw-금액') as HTMLInputElement).value = asset. ? Number(asset..replace(/,/g, '')).toLocaleString() : '';
(document.getElementById('sw-수량') as HTMLInputElement).value = String(asset.);
(document.getElementById('sw-계정명') as HTMLInputElement).value = asset. || '';
(document.getElementById('sw-납품업체') as HTMLInputElement).value = asset. || '';
(document.getElementById('sw-비고') as HTMLInputElement).value = asset. || '';
} else {
document.getElementById('sw-modal-title')!.textContent = `${state.activeSubTab} 자산 추가`;
deleteBtn.style.display = 'none';
(document.getElementById('sw-asset-id') as HTMLInputElement).value = '';
(document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab;
(document.getElementById('sw-법인') as HTMLSelectElement).value = '한맥';
}
}

View File

@@ -0,0 +1,171 @@
import { state } from '../../state';
import { SoftwareAsset, SWUser } from '../../excelHandler';
import { openModal } from './BaseModal';
import { createIcons, Edit2, X, Paperclip } from 'lucide';
let currentSwUserAssetId: string = '';
let tempSwUsers: SWUser[] = [];
/**
* 소프트웨어 사용자 할당 모달 초기화
*/
export function initSWUserModal(renderContent: () => void, closeModals: () => void) {
const btnOpenAddUser = document.getElementById('btn-open-add-user');
const btnSaveEditUser = document.getElementById('btn-save-edit-user');
const btnSaveSwUserMapping = document.getElementById('btn-save-sw-user-mapping');
btnOpenAddUser?.addEventListener('click', () => {
openUserEditModal(-1);
});
btnSaveEditUser?.addEventListener('click', () => {
saveUserEdit();
});
btnSaveSwUserMapping?.addEventListener('click', () => {
// 변경사항 전역 상태에 반영
state.masterData.swUsers = state.masterData.swUsers.filter(u => u.swId !== currentSwUserAssetId);
state.masterData.swUsers.push(...tempSwUsers);
document.getElementById('sw-user-modal')?.classList.add('hidden');
renderContent();
});
// 취소 버튼들
document.getElementById('btn-cancel-sw-user-edit')?.addEventListener('click', () => {
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
});
document.getElementById('btn-cancel-sw-user-modal')?.addEventListener('click', () => {
document.getElementById('sw-user-modal')?.classList.add('hidden');
});
}
/**
* 소프트웨어 사용자 목록 렌더링
*/
function renderUserList() {
const tbody = document.getElementById('user-list-body')!;
tbody.innerHTML = '';
if (tempSwUsers.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="padding: 1rem; text-align: center; color: var(--text-muted);">할당된 사용자가 없습니다.</td></tr>';
return;
}
tempSwUsers.forEach((user, idx) => {
const tr = document.createElement('tr');
tr.style.cssText = 'border-bottom: 1px solid var(--border); transition: background-color 0.2s;';
const deptTeam = [user., user.].filter(Boolean).join(' / ') || '-';
const attachIcon = user. ? `<i data-lucide="paperclip" class="text-primary" style="width:16px; height:16px;" title="${user.}"></i>` : '-';
tr.innerHTML = `
<td style="padding:0.5rem; text-align:left;">${user.}</td>
<td style="padding:0.5rem; text-align:left;">${deptTeam}</td>
<td style="padding:0.5rem; text-align:left;">${user. || '-'}</td>
<td style="padding:0.5rem; text-align:left;"><strong>${user.}</strong></td>
<td style="padding:0.5rem; text-align:center;">${user. || '-'}</td>
<td style="padding:0.5rem; text-align:center; color: var(--text-light);">${attachIcon}</td>
<td style="padding:0.5rem; text-align:center; display:flex; justify-content:center; gap:0.25rem;">
<button type="button" class="btn-icon btn-edit-user" data-idx="${idx}" style="color: var(--primary);" title="수정"><i data-lucide="edit-2" style="width:14px; height:14px;"></i></button>
<button type="button" class="btn-icon btn-remove-user" data-idx="${idx}" style="color: var(--danger);" title="삭제"><i data-lucide="x" style="width:14px; height:14px;"></i></button>
</td>
`;
tbody.appendChild(tr);
});
createIcons({ icons: { Edit2, X, Paperclip } });
tbody.querySelectorAll('.btn-edit-user').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
openUserEditModal(idx);
});
});
tbody.querySelectorAll('.btn-remove-user').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const idx = parseInt((e.currentTarget as HTMLButtonElement).getAttribute('data-idx')!);
tempSwUsers.splice(idx, 1);
renderUserList();
});
});
}
/**
* 사용자 할당 모달 열기
*/
export function openSwUserModal(asset: SoftwareAsset) {
openModal('sw-user-modal');
currentSwUserAssetId = asset.id;
tempSwUsers = state.masterData.swUsers.filter(u => u.swId === asset.id).map(u => ({...u}));
renderUserList();
}
/**
* 사용자 추가/수정 모달 열기
*/
function openUserEditModal(idx: number) {
const editModal = document.getElementById('sw-user-edit-modal')!;
editModal.classList.remove('hidden');
(document.getElementById('edit-user-idx') as HTMLInputElement).value = String(idx);
if (idx === -1) {
document.getElementById('sw-user-edit-modal-title')!.innerText = '새 사용자 추가';
(document.getElementById('new-user-법인') as HTMLSelectElement).value = '한맥';
(document.getElementById('new-user-부서') as HTMLInputElement).value = '';
(document.getElementById('new-user-팀') as HTMLInputElement).value = '';
(document.getElementById('new-user-직위') as HTMLInputElement).value = '';
(document.getElementById('new-user-이름') as HTMLInputElement).value = '';
(document.getElementById('new-user-사용기간') as HTMLInputElement).value = '';
(document.getElementById('new-user-신청서') as HTMLInputElement).value = '';
document.getElementById('new-user-신청서명')!.innerText = '';
} else {
document.getElementById('sw-user-edit-modal-title')!.innerText = '사용자 수정';
const u = tempSwUsers[idx];
(document.getElementById('new-user-법인') as HTMLSelectElement).value = u.;
(document.getElementById('new-user-부서') as HTMLInputElement).value = u.;
(document.getElementById('new-user-팀') as HTMLInputElement).value = u.;
(document.getElementById('new-user-직위') as HTMLInputElement).value = u.;
(document.getElementById('new-user-이름') as HTMLInputElement).value = u.;
(document.getElementById('new-user-사용기간') as HTMLInputElement).value = u.;
(document.getElementById('new-user-신청서') as HTMLInputElement).value = '';
document.getElementById('new-user-신청서명')!.innerText = u. ? `📎${u.}` : '';
}
}
/**
* 사용자 추가/수정 내용 저장 (임시 목록에 반영)
*/
function saveUserEdit() {
const idx = parseInt((document.getElementById('edit-user-idx') as HTMLInputElement).value);
const = (document.getElementById('new-user-법인') as HTMLSelectElement).value;
const = (document.getElementById('new-user-부서') as HTMLInputElement).value;
const = (document.getElementById('new-user-팀') as HTMLInputElement).value;
const = (document.getElementById('new-user-직위') as HTMLInputElement).value;
const = (document.getElementById('new-user-이름') as HTMLInputElement).value.trim();
const = (document.getElementById('new-user-사용기간') as HTMLInputElement).value;
const fileInput = document.getElementById('new-user-신청서') as HTMLInputElement;
let = '';
if (fileInput.files && fileInput.files.length > 0) {
= fileInput.files[0].name;
} else if (idx !== -1) {
= tempSwUsers[idx].;
}
if (!) { alert('이름을 입력해주세요.'); return; }
if (idx === -1) {
tempSwUsers.push({
id: Math.random().toString(36).substring(2, 9),
swId: currentSwUserAssetId,
, , , , , ,
});
} else {
tempSwUsers[idx] = { ...tempSwUsers[idx], , , , , , , };
}
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
renderUserList();
}

View File

@@ -0,0 +1,108 @@
import { state } from '../../state';
import { HardwareAsset } from '../../excelHandler';
import { openModal } from './BaseModal';
/**
* 스토리지 모달 초기화 및 로직 제어
*/
export function initStorageModal(renderContent: () => void, closeModals: () => void) {
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
const btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement;
const btnDeleteStorage = document.getElementById('btn-delete-storage-asset') as HTMLButtonElement;
// 저장 버튼 이벤트
btnSaveStorage?.addEventListener('click', (e) => {
e.preventDefault();
if (!storageForm.checkValidity()) { storageForm.reportValidity(); return; }
const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value;
const fileInput = document.getElementById('storage-품의서') as HTMLInputElement;
const = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('storage-품의서명') as HTMLElement).innerText.replace('📎', '');
const newAsset: HardwareAsset = {
id: id || Math.random().toString(36).substring(2, 9),
type: '스토리지',
: (document.getElementById('storage-법인') as HTMLSelectElement).value,
storage유형: (document.getElementById('storage-유형') as HTMLSelectElement).value,
: (document.getElementById('storage-자산코드') as HTMLInputElement).value,
: (document.getElementById('storage-명칭') as HTMLInputElement).value,
: (document.getElementById('storage-위치') as HTMLInputElement).value,
: '',
IP주소: (document.getElementById('storage-IP주소') as HTMLInputElement).value,
MACaddress: (document.getElementById('storage-MAC주소') as HTMLInputElement).value,
HW사양: '',
OS: '',
: (document.getElementById('storage-모델명') as HTMLInputElement).value,
: (document.getElementById('storage-용량') as HTMLInputElement).value,
_정: (document.getElementById('storage-담당자_정') as HTMLInputElement).value,
_부: (document.getElementById('storage-담당자_부') as HTMLInputElement).value,
: (document.getElementById('storage-구매일') as HTMLInputElement).value,
: (document.getElementById('storage-금액') as HTMLInputElement).value,
: (document.getElementById('storage-납품업체') as HTMLInputElement).value,
};
if (id) {
const idx = state.masterData.hw.findIndex(a => a.id === id);
if(idx !== -1) state.masterData.hw[idx] = newAsset;
} else {
state.masterData.hw.push(newAsset);
}
closeModals();
renderContent();
});
// 삭제 버튼 이벤트
btnDeleteStorage?.addEventListener('click', (e) => {
e.preventDefault();
const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value;
if (confirm('삭제하시겠습니까?')) {
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
closeModals();
renderContent();
}
});
}
/**
* 스토리지 상세 모달 열기
* @param asset 수정 시 자산 데이터, 신규 시 undefined
*/
export function openStorageModal(asset?: HardwareAsset) {
const storageModal = document.getElementById('storage-asset-modal') as HTMLDivElement;
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
const deleteBtn = document.getElementById('btn-delete-storage-asset')!;
openModal('storage-asset-modal');
storageForm.reset();
if (asset) {
document.getElementById('storage-modal-title')!.textContent = '스토리지 상세 정보 수정';
deleteBtn.style.display = 'block';
(document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id;
(document.getElementById('storage-법인') as HTMLSelectElement).value = asset.;
(document.getElementById('storage-유형') as HTMLSelectElement).value = asset.storage유형 || 'NAS';
(document.getElementById('storage-자산코드') as HTMLInputElement).value = asset.;
(document.getElementById('storage-명칭') as HTMLInputElement).value = asset.;
(document.getElementById('storage-위치') as HTMLInputElement).value = asset. || '';
(document.getElementById('storage-모델명') as HTMLInputElement).value = asset. || '';
(document.getElementById('storage-용량') as HTMLInputElement).value = asset. || '';
(document.getElementById('storage-담당자_정') as HTMLInputElement).value = asset._정 || '';
(document.getElementById('storage-담당자_부') as HTMLInputElement).value = asset._부 || '';
(document.getElementById('storage-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
(document.getElementById('storage-MAC주소') as HTMLInputElement).value = asset.MACaddress || '';
(document.getElementById('storage-구매일') as HTMLInputElement).value = asset. || '';
(document.getElementById('storage-금액') as HTMLInputElement).value = asset. ? Number(asset..replace(/,/g, '')).toLocaleString() : '';
(document.getElementById('storage-납품업체') as HTMLInputElement).value = asset. || '';
(document.getElementById('storage-품의서명') as HTMLElement).innerText = asset. ? `📎${asset.}` : '';
} else {
document.getElementById('storage-modal-title')!.textContent = '새 스토리지 자산 추가';
deleteBtn.style.display = 'none';
(document.getElementById('storage-asset-id') as HTMLInputElement).value = '';
(document.getElementById('storage-법인') as HTMLSelectElement).value = '한맥';
(document.getElementById('storage-유형') as HTMLSelectElement).value = 'NAS';
(document.getElementById('storage-품의서명') as HTMLElement).innerText = '';
}
}

37
src/components/Sidebar.ts Normal file
View File

@@ -0,0 +1,37 @@
import { state } from '../state';
export function initSidebar(renderContent: () => void) {
const navItems = document.querySelectorAll('.nav-list li');
const titleElement = document.getElementById('current-tab-title') as HTMLHeadingElement;
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
navItems.forEach(item => {
item.addEventListener('click', () => {
// 탭 UI 업데이트
navItems.forEach(nav => nav.classList.remove('active'));
item.classList.add('active');
// 상태 업데이트
state.activeCategory = item.getAttribute('data-category') as 'hw' | 'sw';
state.activeSubTab = item.getAttribute('data-tab') || '대시보드';
// 타이틀 업데이트 (Deep Green 포인트 컬러 유지)
const catName = state.activeCategory === 'hw' ? '하드웨어' : '소프트웨어';
if (titleElement) {
titleElement.textContent = `${catName} / ${state.activeSubTab}`;
}
// 추가 버튼 노출 여부 (대시보드에서는 숨김)
if (btnAddAsset) {
if (state.activeSubTab === '대시보드') {
btnAddAsset.classList.add('hidden');
} else {
btnAddAsset.classList.remove('hidden');
}
}
// 화면 리렌더링
renderContent();
});
});
}

File diff suppressed because it is too large Load Diff

23
src/state.ts Normal file
View File

@@ -0,0 +1,23 @@
import { MasterAssetData } from './excelHandler';
import { generateDummyData } from './dummyDataGenerator';
// --- State Definitions ---
export interface AppState {
masterData: MasterAssetData;
activeCategory: 'hw' | 'sw';
activeSubTab: string;
activeCharts: any[];
}
// --- Initial State ---
export const state: AppState = {
masterData: generateDummyData(),
activeCategory: 'hw',
activeSubTab: '대시보드',
activeCharts: []
};
// --- State Helpers ---
export function updateState(newState: Partial<AppState>) {
Object.assign(state, newState);
}

View File

@@ -0,0 +1,95 @@
import { state } from '../state';
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 } from 'lucide';
import { openPcModal } from '../components/Modal/PCModal';
import { openHwModal } from '../components/Modal/HWModal';
import { openStorageModal } from '../components/Modal/StorageModal';
import { openSwModal } from '../components/Modal/SWModal';
import { openSwUserModal } from '../components/Modal/SWUserModal';
/**
* 자산 목록 테이블 렌더링 메인 함수
*/
export function renderTable(mainContent: HTMLElement) {
const container = document.createElement('div');
container.className = 'table-container';
const table = document.createElement('table');
if (state.activeCategory === 'hw') {
renderHwTable(table, container, mainContent);
} else {
renderSwTable(table, container, mainContent);
}
// 테이블 내 아이콘 초기화
createIcons({
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 }
});
}
function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) {
const list = state.masterData.hw.filter(a => a.type === state.activeSubTab);
if (state.activeSubTab === '개인PC') {
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산코드</th><th>사용자</th><th>위치</th><th>CPU</th><th>GPU</th><th>RAM</th><th>SSD1</th><th>SSD2</th><th>HDD1</th><th>HDD2</th><th>구매일</th><th>금액</th><th>납품업체</th><th>품의서</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
container.appendChild(table);
mainContent.appendChild(container);
const tbody = document.getElementById('dynamic-tbody')!;
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="17">등록된 자산이 없습니다.</td></tr>`; return; }
list.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `<td>${idx+1}</td><td>${asset.}</td><td>${asset.}</td><td>${asset.||''}</td><td>${asset.||''}</td><td>${asset.CPU||''}</td><td>${asset.GPU||''}</td><td>${asset.RAM||''}</td><td>${asset.SSD1||'-'}</td><td>${asset.SSD2||'-'}</td><td>${asset.HDD1||'-'}</td><td>${asset.HDD2||'-'}</td><td>${asset.||''}</td><td>${asset.||''}</td><td>${asset.||''}</td><td style="text-align:center;">${asset. ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); });
tr.querySelector('.btn-edit')?.addEventListener('click', () => openPcModal(asset));
tbody.appendChild(tr);
});
} else if (state.activeSubTab === '스토리지') {
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>유형</th><th>자산코드</th><th>명칭</th><th>위치</th><th>모델명</th><th>용량</th><th>담당자(정)</th><th>IP주소</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
container.appendChild(table);
mainContent.appendChild(container);
const tbody = document.getElementById('dynamic-tbody')!;
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="13">등록된 자산이 없습니다.</td></tr>`; return; }
list.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `<td>${idx+1}</td><td>${asset.}</td><td>${asset.storage유형||''}</td><td>${asset.}</td><td>${asset.}</td><td>${asset.||''}</td><td>${asset.||''}</td><td>${asset.||''}</td><td>${asset._정||''}</td><td>${asset.IP주소||''}</td><td>${asset.||''}</td><td>${asset.||''}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); });
tr.querySelector('.btn-edit')?.addEventListener('click', () => openStorageModal(asset));
tbody.appendChild(tr);
});
} else {
table.innerHTML = `<thead><tr><th>No</th><th>법인</th>${state.activeSubTab === '전산비품' ? '<th>유형</th>' : ''}<th>자산코드</th><th>명칭</th><th>위치</th><th>관리자</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
container.appendChild(table);
mainContent.appendChild(container);
const tbody = document.getElementById('dynamic-tbody')!;
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="10">등록된 자산이 없습니다.</td></tr>`; return; }
list.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `<td>${idx+1}</td><td>${asset.}</td>${state.activeSubTab === '전산비품' ? `<td>${asset.||'-'}</td>` : ''}<td>${asset.}</td><td>${asset.}</td><td>${asset.}</td><td>${asset.}</td><td>${asset.||''}</td><td>${asset.||''}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
tr.querySelector('.btn-edit')?.addEventListener('click', () => openHwModal(asset));
tbody.appendChild(tr);
});
}
}
function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) {
const list = state.masterData.sw.filter(a => a.type === state.activeSubTab);
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>제품명</th><th>구매일</th><th>수량</th><th>사용가능</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
container.appendChild(table);
mainContent.appendChild(container);
const tbody = document.getElementById('dynamic-tbody')!;
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="7">정보가 없습니다.</td></tr>`; return; }
list.forEach((asset, idx) => {
const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length;
const avail = asset. - assigned;
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `<td>${idx+1}</td><td>${asset.}</td><td>${asset.}</td><td>${asset.||''}</td><td>${asset.}</td><td><strong style="color: ${avail > 0 ? 'var(--primary)' : 'var(--danger)'}">${avail}</strong></td><td style="display:flex; gap:0.25rem;"><button class="btn-outline btn-edit">수정</button><button class="btn-outline btn-users"><i data-lucide="users" style="width:14px; height:14px;"></i></button></td>`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); });
tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset));
tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset));
tbody.appendChild(tr);
});
}

287
src/views/DashboardView.ts Normal file
View File

@@ -0,0 +1,287 @@
import { state } from '../state';
import { HardwareAsset, SoftwareAsset } from '../excelHandler';
/**
* 대시보드 렌더링 메인 함수
*/
export function renderDashboard(mainContent: HTMLElement) {
mainContent.innerHTML = '';
// 기존 차트 리소스 해제
state.activeCharts.forEach(c => c.destroy());
state.activeCharts = [];
if (state.activeCategory === 'hw') {
renderHwDashboard(mainContent);
} else {
renderSwDashboard(mainContent);
}
}
// --- 하드웨어 대시보드 ---
function renderHwDashboard(container: HTMLElement) {
const types = ['개인PC', '서버', '스토리지', '전산비품'];
const units = ['대', '대', '대', '개'];
const groups: any = {};
types.forEach(t => { groups[t] = { idle: [], active: [], aged: [], normal: [] }; });
state.masterData.hw.forEach(a => {
if (!groups[a.type]) return;
if (isHwIdle(a)) groups[a.type].idle.push(a);
else groups[a.type].active.push(a);
const ageY = getHwAgeYears(a);
const isAged = a.type === '전산비품' ? ageY >= 3 : ageY >= 5;
if (isAged) groups[a.type].aged.push(a);
else groups[a.type].normal.push(a);
});
// 사용현황 카드 생성
let usageCards = '';
types.forEach((t, i) => {
const total = groups[t].idle.length + groups[t].active.length;
const used = groups[t].active.length;
const per = total > 0 ? Math.round((used / total) * 100) : 0;
const barColor = per >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)';
usageCards += `
<div class="dashboard-card" data-action="idle" data-type="${t}" style="padding: 1.25rem 1.5rem; cursor:pointer;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 0.5rem;">
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">${t} 사용현황</span>
</div>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">
${total}${units[i]}${used}${units[i]} 사용 중 · 유휴 ${groups[t].idle.length}${units[i]}
</div>
<div style="display:flex; justify-content:space-between; align-items:flex-end; margin-bottom: 0.5rem;">
<div style="font-size: 2rem; font-weight:700; color:${barColor}; line-height:1;">${per}%</div>
<div style="font-size:0.75rem; color:${per >= 50 ? '#4a8220' : 'var(--dash-danger)'}; background:${per >= 50 ? 'var(--dash-light)' : '#fef2f2'}; padding:4px 8px; border-radius:4px; font-weight:500;">
${per >= 50 ? '잘 사용하고 있어요' : '점검이 필요합니다'}
</div>
</div>
<div style="width:100%; height:4px; background-color:var(--border-color); border-radius:2px; overflow:hidden; margin-top:0.25rem;">
<div style="width:${per}%; height:100%; background-color:${barColor};"></div>
</div>
</div>`;
});
// 노후화 카드 생성
let agedCards = '';
types.forEach((t, i) => {
const total = groups[t].aged.length + groups[t].normal.length;
const agedCount = groups[t].aged.length;
const agedPer = total > 0 ? Math.round((agedCount / total) * 100) : 0;
const threshold = t === '전산비품' ? '3년' : '5년';
agedCards += `
<div class="dashboard-card" data-action="aged" data-type="${t}" style="padding: 1.25rem 1.5rem; flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer;">
<div style="flex:1;">
<div style="display:flex; align-items:center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">${t} 노후화 현황</span>
<span style="font-size:0.75rem; color:#bfbfbf; background:#f9f9f9; padding:2px 6px; border-radius:4px;">${threshold} 초과</span>
</div>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1.25rem;">
전체 ${total}${units[i]}${agedCount}${units[i]} 노후 장비
</div>
<div style="font-size: 1.5rem; font-weight:700; color:${agedCount > 0 ? 'var(--dash-danger)' : 'var(--text-main)'};">${agedCount}${units[i]}</div>
</div>
<div style="width: 80px; height: 80px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${agedPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
<div style="width: 64px; height: 64px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
<span style="font-size: 1rem; color:var(--text-muted); font-weight:600;">${agedPer}%</span>
</div>
</div>
</div>`;
});
container.innerHTML = `
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; color: var(--text-main);">사용현황</h3>
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">${usageCards}</div>
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; color: var(--text-main);">노후화 자산 비율</h3>
<div class="dashboard-layout-2col">${agedCards}</div>
`;
// 클릭 이벤트 바인딩
container.querySelectorAll('[data-action="idle"]').forEach(card => {
card.addEventListener('click', () => {
const t = card.getAttribute('data-type')!;
openDashboardDetail(`[${t}] 유휴 자산 목록`, groups[t].idle);
});
});
container.querySelectorAll('[data-action="aged"]').forEach(card => {
card.addEventListener('click', () => {
const t = card.getAttribute('data-type')!;
openDashboardDetail(`[${t}] 노후 장비 목록`, groups[t].aged);
});
});
}
// --- 소프트웨어 대시보드 ---
function renderSwDashboard(container: HTMLElement) {
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
state.masterData.sw.forEach(sw => {
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
const qty = typeof sw. === 'number' ? sw.수량 : parseInt(sw.||'0', 10);
if (sw.type === '구독SW') {
subQty += qty; subUsed += assigned; subTotal++;
if (isSWExpiring(sw)) subExp++;
} else {
permQty += qty; permUsed += assigned; permTotal++;
if (isSWExpiring(sw)) permExp++;
}
});
const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0;
const permPer = permQty > 0 ? Math.round((permUsed/permQty)*100) : 0;
const subExpPer = subTotal > 0 ? Math.round((subExp/subTotal)*100) : 0;
const permExpPer = permTotal > 0 ? Math.round((permExp/permTotal)*100) : 0;
container.innerHTML = `
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
<div class="dashboard-card" data-action="sub-usage" style="padding: 1.25rem 1.5rem; cursor:pointer;">
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 소프트웨어 사용정보</span>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">${subQty}개의 제품 중 ${subUsed}개 사용 중</div>
<div style="display:flex; justify-content:space-between; align-items:flex-end;">
<div style="font-size: 2rem; font-weight:700; color:${subPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};">${subPer}%</div>
</div>
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
<div style="width: ${subPer}%; height: 100%; background-color: ${subPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};"></div>
</div>
</div>
<div class="dashboard-card" data-action="perm-usage" style="padding: 1.25rem 1.5rem; cursor:pointer;">
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">영구 소프트웨어 사용정보</span>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">${permQty}개의 제품 중 ${permUsed}개 사용 중</div>
<div style="display:flex; justify-content:space-between; align-items:flex-end;">
<div style="font-size: 2rem; font-weight:700; color:${permPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};">${permPer}%</div>
</div>
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
<div style="width: ${permPer}%; height: 100%; background-color: ${permPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};"></div>
</div>
</div>
</div>
<div class="dashboard-layout-2col">
<div class="dashboard-card" data-action="sub-exp" style="padding: 1.25rem 1.5rem; cursor:pointer; display:flex; justify-content:space-between; align-items:center;">
<div>
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정</span>
<div style="font-size: 0.8125rem; color:var(--text-muted);">${subExp}개 만료 예정</div>
</div>
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0);"></div>
</div>
<div class="dashboard-card" data-action="perm-exp" style="padding: 1.25rem 1.5rem; cursor:pointer; display:flex; justify-content:space-between; align-items:center;">
<div>
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정</span>
<div style="font-size: 0.8125rem; color:var(--text-muted);">${permExp}개 만료 예정</div>
</div>
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0);"></div>
</div>
</div>
`;
// 클릭 이벤트 바인딩
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => {
openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW'));
});
container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => {
openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '영구SW'));
});
container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => {
openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '구독SW' && isSWExpiring(sw)));
});
container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => {
openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '영구SW' && isSWExpiring(sw)));
});
}
// --- 공통 헬퍼 함수들 ---
function isHwIdle(a: HardwareAsset) {
if (a.type === '개인PC') return !a. || a..trim() === '' || a..trim() === '-';
if (a.type === '스토리지') return !a._정 || a._정.trim() === '' || a._정.trim() === '-';
return !a. || a..trim() === '' || a..trim() === '-';
}
function getHwAgeYears(a: HardwareAsset) {
if (!a.) return 0;
return (Date.now() - new Date(a.).getTime()) / (1000 * 60 * 60 * 24 * 365.25);
}
function isSWExpiring(sw: SoftwareAsset) {
if (sw.type === '구독SW' && sw.) {
const parts = sw..split('~');
if (parts.length > 1) {
const endStr = parts[1].trim();
const endMs = new Date(endStr.replace(/\./g, '-')).getTime();
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
return diffDays >= 0 && diffDays <= 30;
}
} else if (sw.type === '영구SW' && sw. && sw..includes('유지보수: ~')) {
const endStr = sw..split('~')[1].trim();
const endMs = new Date(endStr.replace(/\./g, '-')).getTime();
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
return diffDays >= 0 && diffDays <= 30;
}
return false;
}
// --- 대시보드 상세 모달 제어 (main.ts의 함수 재사용 또는 이동 필요) ---
// 일단 main.ts에 있는 함수를 전역에서 가져와 쓸 수 없으므로, 여기서 직접 정의하거나 main.ts에서 export 해야 합니다.
// 구조 개선을 위해 main.ts에서 이 함수들도 DashboardView로 옮기는 것이 좋습니다.
function openDashboardDetail(title: string, list: HardwareAsset[]) {
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
const thead = tbody.closest('table')!.querySelector('thead')!;
titleEl.textContent = title;
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>자산코드</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매일</th><th>금액</th></tr>`;
tbody.innerHTML = '';
if (list.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;">해당 조건의 자산이 없습니다.</td></tr>`;
} else {
list.forEach((asset, idx) => {
let manager = asset. || asset. || asset._정 || '-';
let name = asset. || asset. || '-';
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${asset.type}</td><td>${asset.}</td><td>${name}</td><td>${asset.||'-'}</td><td>${manager}</td><td>${asset.||'-'}</td><td>${asset.||'-'}</td>`;
tbody.appendChild(tr);
});
}
modal.classList.remove('hidden');
}
function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
const thead = tbody.closest('table')!.querySelector('thead')!;
titleEl.textContent = title;
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>법인</th><th>제품명</th><th>수량</th><th>금액</th></tr>`;
tbody.innerHTML = '';
list.forEach((sw, idx) => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${sw.type}</td><td>${sw.}</td><td>${sw.}</td><td>${sw.}</td><td>${sw.}</td>`;
tbody.appendChild(tr);
});
modal.classList.remove('hidden');
}
function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
const thead = tbody.closest('table')!.querySelector('thead')!;
titleEl.textContent = title;
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
tbody.innerHTML = '';
list.forEach((sw, idx) => {
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
const avail = (typeof sw. === 'number' ? sw.수량 : parseInt(sw.||'0', 10)) - assigned;
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${sw.}</td><td>${sw.}</td><td>${sw.}</td><td>${assigned}</td><td>${avail}</td>`;
tbody.appendChild(tr);
});
modal.classList.remove('hidden');
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}