134 Commits

Author SHA1 Message Date
이태훈
87459c8f44 cleanup: 시스템 구동에 불필요한 백업, 로그 및 비필수 문서 파일 정리
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 12s
ITAM Docker Build Check / docker-build-check (push) Successful in 29s
2026-06-25 20:19:57 +09:00
이태훈
4d98d9a48e docs: 이슈 보고서 및 thoon 브랜치 작업 가이드라인 추가 2026-06-25 20:15:16 +09:00
이태훈
1da75e4abd feat: 테이블 가로 스크롤 및 컬럼 리사이징 개선, 검색 결과 개수 필터 우측 배치, PC부품 제조사 컬럼 삭제 2026-06-25 20:14:12 +09:00
이태훈
3e26420945 feat: 자산 목록 필터 통일화 (자산위치 제거 및 상태 필터 추가) 2026-06-25 17:51:34 +09:00
이태훈
8e22c1d713 fix: 테이블 리사이저 드래그 시 이웃 컬럼 영향 없이 단방향(드래그 방향)으로만 너비가 바뀌도록 개선
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 13s
ITAM Docker Build Check / docker-build-check (push) Successful in 37s
2026-06-25 17:18:26 +09:00
이태훈
8747b3946f feat: 모든 자산 목록 뷰에 마우스 드래그를 이용한 테이블 컬럼 너비 조절(Resizable Columns) 기능 추가 2026-06-25 17:16:44 +09:00
이태훈
ed3d8812c2 config: 운영 DB 접속 환경변수를 내부 컨테이너(itam-mysql) 사양으로 수정
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 13s
2026-06-25 14:29:28 +09:00
이태훈
5588fae6f9 config: 로컬/배포 공통 백엔드 PORT 설정을 3000으로 원상 복구
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 13s
2026-06-25 14:26:45 +09:00
이태훈
e6afe2b6d3 fix: 배포 워크플로우 내 checkout conflict 방지를 위해 로컬 .env 자동 초기화 추가
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 13s
2026-06-25 14:22:31 +09:00
이태훈
9049b60ee5 config: 운영 DB 호스트 주소를 172.16.10.175로 수정
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 13s
2026-06-25 14:21:21 +09:00
이태훈
a5c4a15fab fix: .dockerignore에 mysql_data, scratch, *.sql 제외 규칙 추가하여 빌드 에러 수정
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 14s
2026-06-25 14:15:37 +09:00
이태훈
1ab59bc9e1 fix: Dockerfile.frontend.prod에 mobile.html 복사 단계 추가하여 빌드 에러 수정
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 13s
ITAM Docker Build Check / docker-build-check (push) Successful in 16s
2026-06-25 14:13:55 +09:00
이태훈
7389ed2d82 feat: PC 목록의 현 사용자를 '사용자'로 변경 및 상태가 '재고'일 때 직전 사용자 표시
Some checks failed
ITAM Code Check / build-and-config-check (push) Successful in 13s
ITAM Docker Build Check / docker-build-check (push) Failing after 32s
2026-06-25 14:03:40 +09:00
이태훈
a73dd76e70 feat: 자산 추가/수정 모달에 사용자 자동완성(사번, 직급, 부서 자동 연계) 기능 추가 2026-06-25 13:58:25 +09:00
이태훈
cbfc1bcd1d feat: 자산 정보 수정 시 메모, 용도, 접속정보, 유형 변경 사항도 시스템 이력에 기록되도록 개선 2026-06-25 13:54:49 +09:00
이태훈
6ed939c6bf feat: CPU, GPU, RAM 입력 시 부품 마스터 기준 정합성 검증 추가 및 기존 데이터 정제 2026-06-25 13:41:31 +09:00
이태훈
1ecee53966 Merge branch 'origin/QR_setting' into thoon 2026-06-25 11:39:36 +09:00
이태훈
322a8ae882 fix: 자산번호 생성 시 세부유형 접두사 우선 조회하도록 개선 및 모니터 접두사 MON 추가 2026-06-25 11:24:13 +09:00
이태훈
8176180e52 fix: 프론트엔드 API 호출 시 하드웨어 모달 및 맵 에디터에서 포트 3000 하드코딩 제거하고 상대 경로 프록시 사용하도록 수정 2026-06-25 10:48:52 +09:00
이태훈
2137ee364c fix: 자산 추가/수정 모달에서 서비스 구분 필드 노출 조건 수정 (구분이 서버 또는 유형이 서버PC인 경우만) 2026-06-25 10:30:26 +09:00
이태훈
afd89322bb fix: 카테고리별 자산 목록 화면의 데이터 소스 참조 및 백엔드 맵핑 오류 수정 (PC부품, 공간정보장비, 선물, 사무가구) 2026-06-25 10:26:54 +09:00
이태훈
1457bf277f Merge branch 'main' into thoon 2026-06-25 10:23:02 +09:00
이태훈
0bfff08af6 fix(audit): 실사 승인 대기 목록 필터링 오류 수정 및 승인완료 배지 표시 추가
- GET /api/audit/pending API에서 PENDING 상태인 내역만 반환하도록 SQL 쿼리 수정
- 실사 승인 및 맵 저장 시 asset_location.location_detail에 표준 상세 위치가 저장되도록 개선
- 실사 승인 완료된 자산에 대해 상세 모달, 위치 보기, 목록 보기에 '승인완료' 배지 노출 처리
- 목록 보기에서 기존 위치 표시 형식 및 툴팁을 훼손하지 않도록 배지를 분리하여 렌더링
2026-06-24 17:56:41 +09:00
SDI
ae1fd4b121 feat: 도커 DB생성 compose파일 수정(현재 main 브랜치에서 pull해서 적용)
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 12s
ITAM Docker Build Check / docker-build-check (push) Successful in 16s
2026-06-24 16:19:33 +09:00
이태훈
1eca0ede91 fix: allowedHosts 설정 추가 및 모바일 QR 코드 스캔 시 줄바꿈/공백 정규식 정제 로직 적용 2026-06-23 17:15:48 +09:00
이태훈
f36e8e93e2 feat: QR 자산 스캔 점검, 모바일 웹뷰 및 관리자 승인 시스템 구현 (DB 기반 맵 좌표 저장 단일화 포함) 2026-06-23 16:39:14 +09:00
이태훈
9f165faf13 feat: add desktop label printer program files and clean up backup files 2026-06-23 14:07:22 +09:00
이태훈
577f138533 fix: 위치보기 수정 (도면 오버플로우 제한 및 API 호출 경로 정상화)
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 18s
2026-06-22 13:58:01 +09:00
이태훈
237ac9ee25 fix: 위치보기 수정 (도면 오버플로우 제한 및 API 호출 경로 정상화) 2026-06-22 13:56:52 +09:00
이태훈
aacd2fe7db fix: copy map_editor.html to docker builder stage
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 17s
ITAM Docker Build Check / docker-build-check (push) Successful in 21s
2026-06-22 11:46:26 +09:00
이태훈
90403a1acd fix: comment out obsolete COPY img in Dockerfile.frontend.prod 2026-06-22 11:42:42 +09:00
이태훈
6a76f6968b fix: convert scripts/backup.sh line endings from CRLF to LF 2026-06-22 11:36:50 +09:00
이태훈
621b05a890 chore: clean up build artifacts, temporary excel locks, duplicate plans, and commit current project state
Some checks failed
ITAM Code Check / build-and-config-check (push) Successful in 18s
ITAM Docker Build Check / docker-build-check (push) Failing after 21s
2026-06-22 11:26:26 +09:00
7b631ab858 fix: 원격접속 자산 데이터 매핑 보완
Some checks failed
ITAM Code Check / build-and-config-check (push) Successful in 20s
ITAM Docker Build Check / docker-build-check (push) Failing after 22s
2026-06-19 18:03:58 +09:00
9735344f37 Merge remote-tracking branch 'origin/main'
Some checks failed
ITAM Code Check / build-and-config-check (push) Successful in 21s
ITAM Docker Build Check / docker-build-check (push) Failing after 23s
# Conflicts:
#	src/views/List/ListFactory.ts
2026-06-19 17:58:22 +09:00
SDI
67e3be028b fix: enable location map box clicks
All checks were successful
ITAM Docker Build Check / docker-build-check (push) Successful in 25s
ITAM Code Check / build-and-config-check (push) Successful in 20s
2026-06-19 17:31:37 +09:00
SDI
58f93c959d fix: use proxied api routes in frontend
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 20s
ITAM Docker Build Check / docker-build-check (push) Successful in 26s
2026-06-19 17:20:06 +09:00
4231acc691 Merge branch 'db_setting'
Some checks failed
ITAM Code Check / build-and-config-check (push) Successful in 21s
ITAM Docker Build Check / docker-build-check (push) Failing after 38s
2026-06-19 16:26:16 +09:00
f41f2378d7 fix: 자산번호 저장 누락 오류 수정 및 위치보기 도면 배치 보완 2026-06-19 16:25:28 +09:00
SDI
662f720c6a fix: use updated backup tooling before deploy cleanup
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 20s
ITAM Docker Build Check / docker-build-check (push) Successful in 19s
2026-06-19 15:58:39 +09:00
SDI
5678e28c66 fix: load local env file correctly in backup script
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 20s
2026-06-19 15:53:04 +09:00
41406f56e8 Merge branch 'ux_setting' into db_setting
# Conflicts:
#	README.md
2026-06-19 15:48:26 +09:00
SDI
15c5cbaca2 Merge remote-tracking branch 'origin/main'
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 21s
ITAM Docker Build Check / docker-build-check (push) Successful in 26s
2026-06-19 15:47:18 +09:00
SDI
84d35c1409 fix: smoke check관련 수정 2026-06-19 15:37:50 +09:00
SDI
07eb48f27c chore: force update smoke check to direct backend health check 2026-06-19 15:28:50 +09:00
SDI
fb45c38107 fix: update smoke checks to use port 9090 and direct backend health check2 2026-06-19 15:25:52 +09:00
SDI
6c21e4816e fix: update smoke checks to use port 9090 and direct backend health check 2026-06-19 15:21:11 +09:00
af578a63bc refactor: 프로젝트 정리 및 최적화 (미사용 파일 제거, 코드 중복 제거, 정적 이미지 빌드 경로 수정)
- 미사용 목업 파일(dummyData.ts, realServerData.ts, server_data.json) 및 중복 기획서 제거

- excelHandler.ts 내 미사용 대용량 엑셀 처리 함수들을 삭제하여 xlsx 의존성 제거 및 클라이언트 빌드 크기 최적화

- ListFactory.ts와 utils.ts 간에 중복으로 존재하던 calculatePcScoreDeductive 함수를 하나로 일원화

- 기획서 및 계획 문서들을 docs/plans/ 하위 폴더로 이동하여 프로젝트 루트 정리

- 정적 이미지 폴더(img/)를 public/img/로 이동하여 프로덕션 빌드 시 로고 및 장비 사진 엑박 오류 해결
2026-06-19 15:12:25 +09:00
e8bc42e5de refactor: CSS 파일 모듈화 및 컴포넌트별 직접 Import 구조 전환 (방안 B)
- HTML 내 CSS link 태그들을 삭제하고, 각 TS 진입점 파일에서 CSS 파일을 직접 import하도록 연동

- 스타일 파일들을 각 컴포넌트/뷰 디렉토리 옆으로 이동 배치 (Co-location)

- guide.css, modal.css, dashboard.css, table.css, map-editor.css 이동 및 경로 갱신

- 디자인 시스템(common.css) 및 로그인 스타일(login.css)은 전역 배치 유지하고 main.ts에서 통합 임포트
2026-06-19 15:04:36 +09:00
587e92a7da feat: 서버 탭 전환 시 뷰 모드 유지 및 대시보드/맵 에디터 스타일 표준화
- 서버 탭 복귀 시 최근 선택한 뷰 모드(목록/위치) 상태 유지 및 currentViewMode 상태 일원화

- 개인PC 대시보드 및 맵 에디터의 인라인 CSS 스타일을 공통 CSS 및 변수 클래스로 분리 및 가독성 개선

- Vite 멀티페이지 빌드 설정(vite.config.ts) 추가
2026-06-19 14:55:25 +09:00
SDI
e208e52ed9 feat: implement container-based backup using docker exec 2026-06-19 14:34:09 +09:00
SDI
5dbf69e963 fix: 백업 디렉토리 경로 설정 수정 2026-06-19 14:02:36 +09:00
SDI
d771b28d88 fix: change port from 80 to 9090 2026-06-19 13:51:40 +09:00
c6515c1b5d merge: 공동작업자 HW_Dashboard 브랜치 병합 (대시보드 UI 및 가독성 개선 사항 병합) 2026-06-19 13:39:29 +09:00
e128634e05 style: UI 가독성 개선 및 LocationView 타입 오류 수정
1. common.css의 --mute 변수 색상 대비값 강화 (#71717a) 및 누락된 자산 상태/성능 등급 배지 CSS 클래스 정의
2. ListFactory.ts에서 테이블 헤더(th) 정렬을 데이터 셀(td)과 일치시키고 장문 생략 시 툴팁(title) 추가
3. common.css에서 타이포그래피 스케일 계산식을 clamp에서 max로 변경하여 상한선 제한 해제 (와이드 화면 대응)
4. LocationView.ts 내 HardwareAsset 타입에 정의되지 않은 asset_purpose를 any로 타입 캐스팅하여 TS2339 빌드 에러 해결
5. 프로젝트 폴더 내 일회성 점검/이관 스크립트 및 Playwright 임시 캡처 로그/이미지 파일 정리
2026-06-19 13:19:25 +09:00
6848baae5f Merge pull request 'feat: 자산관리 시스템 도커 운영 배포 환경 및 CI/CD 구축' (#21) from feature/docker-deploy into main
All checks were successful
ITAM Docker Build Check / docker-build-check (push) Successful in 18s
ITAM Code Check / build-and-config-check (push) Successful in 20s
Reviewed-on: #21
2026-06-19 10:50:17 +09:00
SDI
a0570e88d4 fix: Dockerfile 불필요한 이미지 복사 명령어 제거
All checks were successful
ITAM Code Check / build-and-config-check (pull_request) Successful in 20s
ITAM Docker Build Check / docker-build-check (pull_request) Successful in 21s
2026-06-19 10:48:01 +09:00
SDI
502e5059b7 Merge remote-tracking branch 'origin/main' into feature/docker-deploy
Some checks failed
ITAM Code Check / build-and-config-check (pull_request) Successful in 22s
ITAM Docker Build Check / docker-build-check (pull_request) Failing after 22s
2026-06-19 10:27:37 +09:00
c0ef52deac style(table): optimize for 1920x1080 without horizontal scroll
- Switched to fixed table layout to ensure fit within viewport
- Implemented automatic ellipsis (...) for overflowing text in all cells
- Synchronized cell widths and alignment in ListFactory with column definitions
- Removed restrictive min-widths that caused horizontal scrolling
2026-06-18 20:41:03 +09:00
aab1f91d3d feat(map): implement robust ID-based asset mapping and fix UI rendering inconsistencies
- Migrated map mapping from fuzzy coordinates to precise asset_id tracking
- Updated MapEditor to allow explicit asset assignment via dropdown
- Fixed LocationView rendering logic to search across all hardware categories
- Standardized map indicators to always render as areas (boxes) with minimum size
- Restored stable CSS max-height for detail modal photos to prevent clipping
- Synced MapEditor saves directly to database via asset_id
2026-06-18 19:49:15 +09:00
f656f0a439 fix: 대시보드 사양 적정성 직무 매핑 수정 (system_users.position 우선 참조)
- HwDashboard: asset_core.user_position 대신 system_users.user_name -> position 으로 세부 직무 조회
- ListFactory: 동일하게 세부 직무명 우선 참조
- 미니 모달 조직(직무) 컬럼: _resolved_position 사용으로 정확한 직무명 표시
- 수정된 필드명: u.name -> u.user_name (system_users 실제 컬럼명 반영)
- 예) 디자이너(3D, 영상) 직군이 최상급 기준으로 올바르게 판정됨
2026-06-18 19:48:23 +09:00
e77c4854cb fix: restore exact matching logic for map locations 2026-06-18 17:04:25 +09:00
1d32a0350b feat: 등급별 자산 종합 현황 및 사양 적정성 분석 레이아웃 5:5 콤팩트 최적화 2026-06-18 15:56:51 +09:00
SDI
d54997cd55 fix: prepare ci env for compose validation 2026-06-18 13:52:05 +09:00
SDI
fa8dec1780 feat:CI/CD Gitea 워크플로우 등 누락 파일 반영 2026-06-18 13:39:35 +09:00
309c400ee2 최신코드 반영 2026-06-18 13:00:18 +09:00
3db05f2939 feat(ui/ux): unify typography to Pretendard and enforce read-only view mode as default
- Set global font-family to Pretendard and letter-spacing to -0.02em.
- Standardized table header font-size to var(--fs-sm).
- Fixed table clipping and sticky header behavior at 1920x1080.
- Implemented dynamic select options in search filters.
- Enforced 'view' mode as default for all asset modals (PC, Server, SW, etc.).
- Improved Modal logic to ensure all fields (including dynamic rows) are correctly locked.
- Updated Location View detail button from 'Edit' to 'View'.
- Updated design_rule.md to reflect new typography standards.
2026-06-18 11:13:16 +09:00
2cb4b87c0a style: surgically unify Admin page UI and sub-tabs
- Standardized sub-tab rendering in PartsMasterListView to prevent duplication
- Updated badge classes to match system design guide (badge-success/warning)
- Refactored JobSpecModal to use unified .grid-form and header identity
- Preserved all functional logic and tab-switching behavior from collaborator
2026-06-17 13:16:15 +09:00
6ed2faee2d merge: remote main updates into ux_setting with style preservation
- Resolved conflicts in state.ts, HwDashboard.ts, ListFactory.ts, and PartsMasterListView.ts
- Prioritized latest functional logic from main branch (Job Spec mapping, Matrix calculations)
- Maintained Vercel-inspired UI styling and unified CSS classes from ux_setting branch
- Synchronized PC status toggle visibility rules with latest main branch changes
2026-06-17 13:08:59 +09:00
89d3ac2e89 style: unify UI styling & restore dashboard logic
- Restored HW/SW Dashboard full features (Chart.js, filters, tables) from main
- Unified Search Bar & Filter Bar across all views (List, Location)
- Integrated asset identity info into all Modal Headers
- Standardized 'Remove Row' buttons as high-visibility circular circles
- Centralized hardcoded inline styles into dedicated CSS files
- Fixed various ReferenceErrors and layout regressions in HWModal
2026-06-17 12:29:26 +09:00
9d19d8283e 자산관리 시스템 도커라이징 2026-06-17 11:31:10 +09:00
b37981506e style: revert content/logic to main while preserving Vercel UI styles
- Reverted HWModal to unified form structure from main branch
- Restored original field positions and visibility logic in all modals
- Applied Vercel-inspired CSS classes and removed legacy inline styles
- Restored SwDashboard 2x2 layout from main
- Cleaned up unused modular form files
- Fixed TypeError related to ASSET_MFR schema key
2026-06-17 10:46:24 +09:00
abc531a41e Design: 대시보드 하단 표 세로비율 확장 및 스크롤바 제거 2026-06-17 09:28:06 +09:00
8451101325 Style: 대시보드 UI 프리미엄 리스타일링 및 카드 구조 도입 2026-06-17 09:25:16 +09:00
3e69e74bc9 Feat: 통합 사양 적정성 인라인 바 그래프 및 대시보드 레이아웃 개편 2026-06-17 09:22:31 +09:00
73ef13f3a5 style: apply Vercel-inspired responsive UI & fluid scaling 2026-06-16 17:43:20 +09:00
155570e8de style: disable global text selection to prevent accidental UI dragging 2026-06-16 14:32:16 +09:00
723c4723f6 feat: 개인PC 자산 관리(PC) 페이지에서 '자산 현황' 토글 탭 숨김처리 및 자산 목록으로 기본 고정 2026-06-15 15:01:46 +09:00
a44283281f feat: 대시보드 미니 모달 상세 자산 목록 테이블에 등급 및 점수 컬럼 표기 기능 추가 2026-06-15 14:53:36 +09:00
fa87f383e2 fix: 직무별 기준 사양 대비 부족 사양(0.6배 미만) 및 오버스펙(1.5배 초과) 판정 수치 비율 임계치 조정 2026-06-15 14:51:09 +09:00
6118141f6e feat: 직무별 기준 사양(job_spec_standards) 데이터를 활용한 부족사양, 오버스펙 판정 및 등급별 부족분(구매 필요) 산출 공식 연동 2026-06-15 14:43:53 +09:00
119c799d1d style: 레이아웃 비율 복구 및 타이포그래피 전역 표준화 (16px Base)
- 주요 변경 사항:
  1. 레이아웃 안정화: 서버 위치도 뷰의 2:1 비율 복원 및 가변형(Adaptive) 레이아웃 적용
  2. 타이포그래피 표준화: 전역 폰트 스케일 도입 및 기본 폰트 사이즈 상향 (15px -> 16px)
  3. 3-Way 토글 통합: [자산 위치] [운영 현황] [자산 목록] 간의 전환 오류 수정 및 UI 통일
  4. 하드코딩 제거: 인라인 스타일을 CSS 클래스 및 변수 체계로 전면 리팩토링
  5. 가이드 업데이트: 변경된 디자인 정책을 design_rule.md에 반영
2026-06-15 14:21:54 +09:00
05e23883b8 feat: 직무별 기준 사양 등록 시 부품 마스터 자동완성 검색 및 성능 점수 실시간 자동 계산 기능 추가 2026-06-15 13:29:10 +09:00
8c406fd0b8 fix: 부품 표준 정보 서브 탭 렌더링 시 DOM 쿼리 타이밍 에러 해결 (document.getElementById -> tabContainer.querySelector) 2026-06-15 13:27:20 +09:00
e678f9d653 feat: 부품 마스터 화면 내 직무별 기준 사양 CRUD 및 서브 탭 연동 기능 추가 2026-06-15 13:26:11 +09:00
132e37d0d3 refactor: 사양 부족 및 노후 장비의 교체 수요를 해당 사용자의 직무 권장 사양 등급에 맞추어 분산 합계 계산 2026-06-15 13:19:10 +09:00
d6e75f8b2c refactor: 교체 대상 PC 교체 수요를 보급 PC 신규 구매 필요 수량으로 이전 합산 2026-06-15 13:17:01 +09:00
c35f57acab style: 보급 PC 라벨 단순화 및 부족분 컬럼명을 구매 필요로 변경 2026-06-15 13:14:45 +09:00
97cecb8b50 feat: 5등급 PC 분류 체계 도입 (교체 대상 PC 등급 신설) 2026-06-15 13:12:07 +09:00
b9d28736e2 docs: add work log for 2026-06-15 and update DB deletion policy in README 2026-06-15 11:47:46 +09:00
a4b620099c feat: 대시보드 폰트 크기, 패딩 조절 및 자산목록 정렬 기준 변경(updated_at 내림차순) 2026-06-15 11:22:07 +09:00
b169176d57 WIP(style): UI 컴포넌트 하드코딩 제거 및 CSS 통합 (진행 중)
- 작업 상태: 진행 중 (Work In Progress)
- 주요 변경 사항:
  1. CSS 파일 통합: HWModal, SWModal, ListFactory 등에서 인라인 스타일(style 속성) 전면 제거 및 클래스 기반으로 재작성
  2. 폰트/타이포그래피 스케일업: 최소 폰트 14px 기준으로 전체 텍스트 크기 상향 및 굵기(font-weight) 상향 조정
  3. GNB(상단바) 레이아웃 개편: 2단 구조(로고 라인 / 메뉴 라인)로 변경 및 카테고리 텍스트 라벨 생략을 통한 간결화
  4. 로고 이미지 교체: image 92.png로 업데이트 및 경로 정리
  5. 디자인 가이드 분리: README에서 design_rule.md로 디자인 정책 문서 독립

* 참고: 현재 디자인 검토를 위한 중간 반영 상태이며, 피드백에 따라 추가 수정 예정임.
2026-06-12 15:57:20 +09:00
56abdddbc7 Merge remote-tracking branch 'origin/main' into ux_setting 2026-06-12 13:34:13 +09:00
fd9e88d7c6 style: 리팩토링 및 CSS 통합 작업 완료 (하드코딩 스타일 제거) 2026-06-12 13:29:59 +09:00
407b9ba531 style: 연도별 PC 노후도 예측 표의 왼편 여백 제거 및 레이아웃 정렬 2026-06-12 10:40:37 +09:00
55c43aa250 merge: remote main updates into local main 2026-06-12 10:37:52 +09:00
9186eb50ca feat: 자산 관리 시스템 고도화 및 DB 정규화 대응 수정
1. 자산 저장 시 500 에러 해결: V3 정규화 스키마에 맞춰 테이블 매핑 최신화 및 저장 로직 안정화
2. 자산 번호 체계 개편: 구매일자(YYYYMM)와 유형을 기반으로 PREFIX-YYYYMM-NNNN 규칙 적용 (코드 로직 수정 및 기존 데이터 전량 갱신)
3. 구매일자 표준화: 모든 purchase_date를 YYYY-MM-DD 형식으로 통일
4. HWModal 기능 복원: 위치 등록 시 다중 사진 페이지네이션(좌우 버튼) 기능 복구
5. 위치 지도 고도화: HTML 인터랙티브 지도 지원 및 이미지 지도 내 좌석 스내핑 로직 추가
2026-06-12 10:29:42 +09:00
8a3727ea61 feat: 대시보드 구분선 디자인 전환, 폰트 확대 및 버그 수정 2026-06-12 08:49:04 +09:00
0c1977f707 style: 폰트 크기를 ux_setting 표준 규격(14px)으로 복구 및 최적화 2026-06-11 13:06:04 +09:00
19e6be27de fix(merge): resolve compile errors and restore remote db IP 2026-06-11 11:49:34 +09:00
accbbdc2fa fix(pc-view): restore pc list view rendering and enable status view toggle for PC 2026-06-11 11:43:51 +09:00
d3c4fa5e66 fix(main): restore dashboard tab rendering check in refreshView 2026-06-11 11:42:03 +09:00
8c1cb6cf93 merge: merge origin/main into HW_Dashboard and resolve conflicts 2026-06-11 11:39:09 +09:00
4810df212a merge: ux_setting 브랜치를 main에 병합 (HWModal UI 고도화 및 PC 탭 제한 사항 반영) 2026-06-11 11:23:09 +09:00
f5a84a77ef feat: PC 페이지 개발 대기 상태 전환 및 자산현황 뷰 노출 범위 제한 (서버 탭 전용) 2026-06-11 11:20:28 +09:00
565802f55b feat(flow-logs): fix text overlapping, show full asset code, filter by current month and support JSON logs 2026-06-11 11:14:04 +09:00
10479aad7e feat: HWModal 네트워크 및 원격 접속 정보 동적 입력 기능 통합 및 UI 최적화 2026-06-11 11:14:00 +09:00
95fbd3f606 fix: 자산 상세 정보 레이아웃 최적화 및 스타일 표준화
- 상세 정보 섹션을 2열 그리드로 변경하여 정보 밀도 향상\n- 항목 제목(12px) 및 내용(14px) 폰트 크기 조정\n- 인라인 스타일을 dashboard.css로 이전 및 중앙 집중화\n- 불필요한 아이콘 제거 및 UI 정돈
2026-06-11 10:37:33 +09:00
207acbdecb fix: LocationView 레이아웃 안정화 및 UI 개선
- 페이지네이션 버튼을 상세위치 옆으로 이동\n- 지도 이미지 밀림 현상 방지를 위한 정렬 방식 수정 및 동기화 로직 보강\n- 상단 메뉴에서 구버전 현황 버튼 제거
2026-06-11 10:08:43 +09:00
164568843b feat: LocationView 고도화 - 지도 클릭 시 사이드바 상세 정보 표시 및 구역 필터링 구현 2026-06-11 09:47:57 +09:00
29c7d5f3d8 fix: 서버 오류 수정 2026-06-10 17:53:52 +09:00
ce1ed40561 feat: 하드웨어 자산 관리 고도화 및 자동 이력 시스템 구축
- 통합 원격 접속 정보 UI 구현 (IP/MAC 및 계정 정보 통합)
- 서버 측 스냅샷 비교 기반 자동 이력(Log) 생성 로직 도입
- 타임라인 UI 개선 (이벤트별 색상 뱃지 및 변동 사항 강조)
- 자산 상세 필드 확장 (서비스 구분, 용도 등)
- 테스트 데이터 생성기 및 이력 계획서 추가
2026-06-10 09:51:03 +09:00
525dbd77d4 feat(nav): restrict admin menu to only display dashboard tab 2026-06-10 09:22:10 +09:00
35c5b1e0fa feat(nav): hide dashboard and admin settings from practitioner role 2026-06-10 09:20:37 +09:00
b87ca2854b feat(role): enable admin login, default Admin to Dashboard, and default Practitioner to Server list 2026-06-10 09:13:46 +09:00
2f88a0fae7 Merge origin/main into HW_Dashboard and resolve conflicts 2026-06-10 09:13:23 +09:00
9a2c35e652 feat(dashboard): update charts and styling on HW_Dashboard 2026-06-10 09:12:29 +09:00
25ebaf4685 refactor: rename asset_network to asset_remote
- DB 테이블명 변경 마이그레이션 스크립트 추가 (migrate_v5_rename_remote.js)

- Backend (server.js): 쿼리 및 매핑 로직을 asset_remote 및 remotes 속성으로 업데이트

- Frontend (HWModal.ts): 폼 필드와 데이터 바인딩을 remotes로 일괄 수정

- 유틸리티 스크립트의 레퍼런스 일괄 업데이트
2026-06-09 18:44:53 +09:00
2b9c965c91 feat: 동적 디스크 확장 기능 및 하드웨어 카테고리 필터링 고도화 2026-06-09 16:29:54 +09:00
4b408b0640 feat: HW 모달 UI 고도화 및 자산 분류 체계 개편 2026-06-09 13:56:05 +09:00
3ab587d342 feat: DB V3 정규화 및 용도 기반 동적 UI 구현
- 백엔드: asset_core(마스터), asset_spec(사양), asset_volume(스토리지), asset_location(위치), asset_network(네트워크/원격) 5개 테이블로 V3 정규화 완료

- 백엔드: /api/assets/master 단일 엔드포인트로 통합 및 서브쿼리 최적화를 통한 UI 하위 호환성 유지

- 백엔드: 저장 로직(save) V3 스키마 분산 저장 및 cascade 기반 삭제 로직 적용

- 프론트엔드(HWModal): '현 용도(current_role)' 필드 추가 및 서버/개인용에 따른 네트워크/위치 섹션 동적 렌더링 구현

- 프론트엔드(state): 분산된 API 호출을 단일 호출로 통합하여 렌더링 성능 최적화

- 레거시 백업 파일 및 불필요한 구형 테이블 완벽 정리 완료
2026-06-08 17:58:48 +09:00
3b9b2ea598 feat: 자산번호 4자리 일련번호 확장 및 날짜 기반 생성 로직 추가
- 백엔드(server.js): 자산번호 자동 생성 시 구매일자(YYYYMM)를 파싱하여 [접두사]-[YYYYMM]-[0000] 형태로 4자리 일련번호를 부여하도록 로직 전면 수정
- 프런트엔드(HWModal.ts): 자산번호 생성 API 호출 시 사용자가 입력한 구매일자 데이터를 파라미터로 함께 전송하도록 연동
- 전체 DB 및 로컬 마스터 데이터의 4자리 일련번호 및 날짜 복구 마이그레이션 반영
2026-06-08 15:18:39 +09:00
05c565552a feat: 자산 현황 고도화 및 데이터 표준화 작업 완료
- UI 개선: 모든 아이콘/이모지 제거(미니멀리즘), 목록 행 하이라이트 추가, 요약 레이아웃 최적화
- 경고 시스템: 위치 부적절/형식 부적절 사유별 세분화 및 상단 통계 배지 적용
- 정보 강화: 우측 패널 '상세 보기' 버튼 복구 및 '자산 유형' 정보 추가 노출
- 표준화: 위치명(기술개발센터, 한맥빌딩) 및 자산번호 접두사(STO/NAS/DAS -> DSS/STM) 통합
- 데이터: realServerData.ts 및 SharedData.ts 내 유형/접두사 표준 체계 전면 반영
- 레이아웃: 헤더 역할 스위처 및 가이드 버튼 일렬 정렬 수정
2026-06-08 11:40:27 +09:00
2ec9261c03 feat: 자산 현황 레이아웃 개선 및 위치 정보 가독성 최적화
- 박스형 디자인 배제 및 선 기반의 미니멀 레이아웃 적용
- 목록 클릭 시 우측 패널에 배치도 및 위치 마커 즉시 표시
- 사진 비율 유지 및 좌표 정밀 보정 (onload/resize 로직 도입)
- 총 보유 자산 통계에 외부(운영)/내부(테스트) 대수 세부 표시 추가
- 빌드 오류 수정을 위한 구문 정리
2026-06-08 09:44:15 +09:00
06f3baaa58 feat: 대시보드 및 자산현황 레이아웃 개편 및 디자인 복원
- 자산현황 대시보드의 그래프를 제거하고 표와 상세정보 패널(5:5 비율)로 레이아웃 개편
- 표에서 '비고' 컬럼을 제거하고 '담당자(정)', '담당자(부)' 컬럼으로 교체 및 너비 조정
- 이중 필터(위치 -> 상세위치) 도입으로 필터링 기능 강화
- 상세정보 패널의 사진 영역을 Flexbox로 최적화하여 위아래 잘림 현상 원천 차단
- 모달창 내 '수정 모드' 폰트 색상을 디자인 가이드(var(--color-dahong))에 맞게 붉은 계열로 원상 복구 및 누락 변수 추가
- ListFactory.ts의 ASSET_SCHEMA.SERVICE_TYPE 참조 시 발생하던 TypeError 픽스
2026-06-05 18:01:26 +09:00
eead43837d feat: PC 맞춤형 대시보드 구현 및 자산 현황 레이아웃 최적화
- PC 자산 관리 화면에 유형별(공용/서버/개인) 통계 및 차트 적용
- 자산 현황 대시보드의 위치 분류 체계 통일 (센터/IDC/한맥빌딩)
- 하단 요약 표의 자산번호 컬럼을 비고(Memo)로 교체 및 말줄임표 적용
- 차트 크기 확대 및 네비게이션 메뉴 레이아웃 안정화
- ListFactory.ts 내 formatInline 미정의 오류 수정
2026-06-05 10:51:29 +09:00
46422e8544 cleanup: remove database migration and utility scripts 2026-06-02 14:44:45 +09:00
a30f99f0ad feat: improve asset code generation and re-sequence assets by year
- Enhanced backend asset code generation logic to handle multiple tables
- Integrated asset code generation button in HWModal
- Included utility scripts for asset code migration and DB synchronization
- Resolved issues with missing purchase dates and duplicate asset codes
2026-06-02 14:40:06 +09:00
9e8ab11f99 feat: implement role-based entry and navigation enhancements
- Replace credential login with Admin/Practitioner role selection
- Add role-switcher toggle in header with automatic reversion for Admin mode
- Implement immediate return to role selection via system logo click
- Integrate role state management into global app state
2026-06-01 17:56:22 +09:00
19d4222470 Merge branch 'main' into login 2026-06-01 16:52:17 +09:00
db5c7a96a6 fix: restore map editor layout and event binding logic 2026-06-01 16:34:57 +09:00
7d3d5ef281 feat: implement initial login UI and entry logic 2026-06-01 16:23:23 +09:00
9cd5d59bf8 refactor: complete modal class-based architecture, design system integration, and map editor modularization 2026-06-01 14:57:07 +09:00
590ddd0e85 feat: enhance map editor, refine location view, and update image assets
- Map Editor: Add box numbering (drawing/placed) and set default file
- Location View: Refine mouse interaction in view mode (readonly)
- Assets: Add MDF room support and update server room directory structure
- Backend: Add map configuration API for real-time saving
2026-06-01 14:00:45 +09:00
214 changed files with 21979 additions and 42535 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
node_modules
dist
build
.git
.gitignore
.env
npm-debug.log
uploads
*.xlsx
*.log
mysql_data
scratch
*.sql

6
.env Normal file
View File

@@ -0,0 +1,6 @@
DB_HOST=itam-mysql
DB_PORT=3306
DB_USER=itam
DB_PASS=itam1234
DB_NAME=itam
PORT=3000

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Database Configuration
DB_HOST=172.16.8.151
DB_PORT=3306
DB_USER=itam_admin
DB_PASS=itam1234
DB_NAME=itam
# Application Configuration
NODE_ENV=development
PORT=3000
# Logging (optional)
LOG_LEVEL=info
# Security (for production)
# API_KEY=your_api_key_here
# JWT_SECRET=your_jwt_secret_here

7
.gitea/coverage.json Normal file
View File

@@ -0,0 +1,7 @@
{
"Path": "./backend/coverage.out",
"Thresholds": {
"baron-sso-backend/internal/handler": 10,
"baron-sso-backend/internal/service": 10
}
}

View File

@@ -0,0 +1,47 @@
name: ITAM Code Check
on:
push:
branches:
- Dockerizing
- main
pull_request:
workflow_dispatch:
jobs:
build-and-config-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Prepare CI env file
run: |
cat <<'EOF' > .env
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=itam_ci
DB_PASS=itam_ci_password
DB_NAME=itam
NODE_ENV=production
PORT=3000
LOG_LEVEL=info
EOF
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Frontend TypeScript and Vite build
run: npm run build
- name: Validate test compose
run: docker compose -f docker-compose.test.yaml config
- name: Validate prod compose
run: docker compose -f docker-compose.prod.yaml config

View File

@@ -0,0 +1,69 @@
name: ITAM Docker Build Check
on:
push:
branches:
- Dockerizing
- main
paths:
- "Dockerfile.frontend.prod"
- "Dockerfile.backend.prod"
- "docker-compose.prod.yaml"
- "docker-compose.test.yaml"
- "docker/**"
- "src/**"
- "server.js"
- "package.json"
- "package-lock.json"
- "vite.config.ts"
- "index.html"
- "img/**"
- "public/**"
pull_request:
paths:
- "Dockerfile.frontend.prod"
- "Dockerfile.backend.prod"
- "docker-compose.prod.yaml"
- "docker-compose.test.yaml"
- "docker/**"
- "src/**"
- "server.js"
- "package.json"
- "package-lock.json"
- "vite.config.ts"
- "index.html"
- "img/**"
- "public/**"
workflow_dispatch:
jobs:
docker-build-check:
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
COMPOSE_DOCKER_CLI_BUILD: "1"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Prepare CI env file
run: |
cat <<'EOF' > .env
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=itam_ci
DB_PASS=itam_ci_password
DB_NAME=itam
NODE_ENV=production
PORT=3000
LOG_LEVEL=info
EOF
- name: Build backend production image
run: docker build -f Dockerfile.backend.prod -t itam-backend:ci .
- name: Build frontend production image
run: docker build -f Dockerfile.frontend.prod -t itam-frontend:ci .
- name: Validate production compose with CI env
run: docker compose -f docker-compose.prod.yaml config

View File

@@ -0,0 +1,143 @@
name: ITAM Production Deploy
on:
workflow_dispatch:
inputs:
target_branch:
description: "Branch to deploy"
required: true
default: "main"
type: string
jobs:
deploy-production:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH agent
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
- name: Validate required production variables
env:
PROD_HOST: ${{ vars.PROD_HOST }}
PROD_USER: ${{ vars.PROD_USER }}
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
PROD_GIT_URL: ${{ vars.PROD_GIT_URL }}
DB_HOST: ${{ vars.PROD_DB_HOST }}
DB_PORT: ${{ vars.PROD_DB_PORT }}
DB_USER: ${{ vars.PROD_DB_USER }}
DB_PASS: ${{ secrets.PROD_DB_PASS }}
DB_NAME: ${{ vars.PROD_DB_NAME }}
run: |
set -euo pipefail
required_keys="PROD_HOST PROD_USER PROD_DEPLOY_PATH PROD_GIT_URL DB_HOST DB_PORT DB_USER DB_PASS DB_NAME"
for key in ${required_keys}; do
if [ -z "${!key:-}" ]; then
echo "::error::Missing required variable or secret: ${key}"
exit 1
fi
done
- name: Create production env file
env:
DB_HOST: ${{ vars.PROD_DB_HOST }}
DB_PORT: ${{ vars.PROD_DB_PORT }}
DB_USER: ${{ vars.PROD_DB_USER }}
DB_PASS: ${{ secrets.PROD_DB_PASS }}
DB_NAME: ${{ vars.PROD_DB_NAME }}
LOG_LEVEL: ${{ vars.PROD_LOG_LEVEL }}
run: |
set -euo pipefail
EFFECTIVE_LOG_LEVEL="${LOG_LEVEL:-info}"
cat > .env.deploy <<EOF
DB_HOST=${DB_HOST}
DB_PORT=${DB_PORT}
DB_USER=${DB_USER}
DB_PASS=${DB_PASS}
DB_NAME=${DB_NAME}
NODE_ENV=production
PORT=3000
LOG_LEVEL=${EFFECTIVE_LOG_LEVEL}
EOF
- name: Deploy to production host
env:
PROD_HOST: ${{ vars.PROD_HOST }}
PROD_USER: ${{ vars.PROD_USER }}
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
PROD_BACKUP_ROOT: ${{ vars.PROD_BACKUP_ROOT }}
PROD_GIT_URL: ${{ vars.PROD_GIT_URL }}
DB_HOST: ${{ vars.PROD_DB_HOST }}
DB_PORT: ${{ vars.PROD_DB_PORT }}
DB_USER: ${{ vars.PROD_DB_USER }}
DB_PASS: ${{ secrets.PROD_DB_PASS }}
DB_NAME: ${{ vars.PROD_DB_NAME }}
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
run: |
set -euo pipefail
ssh-keyscan -H "${PROD_HOST}" >> ~/.ssh/known_hosts
ssh "${PROD_USER}@${PROD_HOST}" "mkdir -p '${PROD_DEPLOY_PATH}'"
ssh "${PROD_USER}@${PROD_HOST}" "if [ ! -d '${PROD_DEPLOY_PATH}/.git' ]; then git clone '${PROD_GIT_URL}' '${PROD_DEPLOY_PATH}'; else cd '${PROD_DEPLOY_PATH}' && git remote set-url origin '${PROD_GIT_URL}'; fi"
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && git checkout -- .env || true && git fetch origin '${TARGET_BRANCH}' && git checkout -B '${TARGET_BRANCH}' FETCH_HEAD && git reset --hard FETCH_HEAD"
EFFECTIVE_BACKUP_ROOT="${PROD_BACKUP_ROOT:-/home/user/dachs_backups}"
ssh "${PROD_USER}@${PROD_HOST}" "export DEPLOY_PATH='${PROD_DEPLOY_PATH}' BACKUP_ROOT='${EFFECTIVE_BACKUP_ROOT}'; sh -eu -s" <<'REMOTE_BACKUP'
case "$BACKUP_ROOT" in
"$DEPLOY_PATH"|"$DEPLOY_PATH"/*)
echo "Backup path must be outside deploy path: $BACKUP_ROOT"
exit 1
;;
esac
if [ -d "$DEPLOY_PATH/.git" ]; then
mkdir -p "$BACKUP_ROOT"
echo "Starting pre-deploy backup..."
cd "$DEPLOY_PATH"
if [ -f Makefile ] && [ -f scripts/backup.sh ] && [ -f .env ]; then
make predeploy-backup ENV_FILE=.env BACKUP_ROOT="$BACKUP_ROOT"
else
echo "Skipping pre-deploy backup because required backup files are missing in current deployment."
fi
else
echo "Skipping pre-deploy backup because no existing deployment was found."
fi
REMOTE_BACKUP
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && git clean -fd"
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && mkdir -p uploads logs/nginx"
scp .env.deploy "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_PATH}/.env"
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && chmod 600 .env && docker compose -f docker-compose.prod.yaml config && docker compose -f docker-compose.prod.yaml up -d --build"
- name: Post-deploy status check
env:
PROD_HOST: ${{ vars.PROD_HOST }}
PROD_USER: ${{ vars.PROD_USER }}
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
run: |
set -euo pipefail
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && docker compose -f docker-compose.prod.yaml ps"
- name: Post-deploy smoke checks
env:
PROD_HOST: ${{ vars.PROD_HOST }}
PROD_USER: ${{ vars.PROD_USER }}
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
run: |
set -euo pipefail
ssh "${PROD_USER}@${PROD_HOST}" "curl -fsS http://localhost:9090/health"
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && docker compose -f docker-compose.prod.yaml exec -T backend curl -fsS http://localhost:3000/health"
- name: Cleanup generated env file
if: ${{ always() }}
run: rm -f .env.deploy

16
.gitignore vendored
View File

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

12
Dockerfile.backend Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "server"]

48
Dockerfile.backend.prod Normal file
View File

@@ -0,0 +1,48 @@
FROM node:20-alpine
LABEL maintainer="ITAM Team <devops@itam.local>"
# Set production environment
ENV NODE_ENV=production
WORKDIR /app
# Install curl for health checks and dumb-init for proper signal handling
RUN apk add --no-cache curl dumb-init mysql-client
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production
# Copy application code
COPY server.js ./
COPY src ./src
# Create non-root user 'appuser' with UID 1001 (1000 already in use by node image)
RUN addgroup -g 1001 appuser && \
adduser -D -u 1001 -G appuser appuser
# Set ownership of application files to appuser
RUN chown -R appuser:appuser /app
# Create logs directory
RUN mkdir -p /app/logs && \
chown -R appuser:appuser /app/logs
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 3000
# Health check - backend should implement /health endpoint
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Use dumb-init from PATH to avoid distro-specific absolute path issues
ENTRYPOINT ["dumb-init", "--"]
# Run application
CMD ["npm", "run", "server"]

12
Dockerfile.frontend Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 8080
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

62
Dockerfile.frontend.prod Normal file
View File

@@ -0,0 +1,62 @@
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig*.json ./
COPY vite.config.ts ./
# Install all dependencies (including devDependencies for build)
RUN npm ci
# Copy source code
COPY src ./src
COPY public ./public
COPY index.html map_editor.html mobile.html ./
# Build application
RUN npm run build
# Verify build output
RUN ls -la dist/ && echo "Build completed successfully"
# Stage 2: Runtime
FROM nginx:stable-alpine
LABEL maintainer="ITAM Team <devops@itam.local>"
# Install curl for health checks
RUN apk add --no-cache curl
WORKDIR /usr/share/nginx/html
# Copy built assets from builder
COPY --from=builder /app/dist .
# Copy static image assets referenced by literal /img/... paths (Obsolete: img folder is now public/img and copied via dist)
# COPY img ./img
# Copy root-level logo asset referenced directly by index.html
# COPY ["image 92.png", "./image 92.png"]
# Copy Nginx static file serving configuration (not reverse proxy)
COPY docker/frontend/default.conf /etc/nginx/conf.d/default.conf
# Create nginx runtime user and directories
RUN mkdir -p /var/log/nginx && \
chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD curl -f http://localhost:80/ || exit 1
# Run nginx
CMD ["nginx", "-g", "daemon off;"]

33
Makefile Normal file
View File

@@ -0,0 +1,33 @@
SHELL := /bin/sh
ENV_FILE ?= .env
BACKUP_ROOT ?= backups
RETENTION_DAYS ?= 14
BACKUP_SCRIPT := scripts/backup.sh
.PHONY: help db-dump files-backup full-backup predeploy-backup cleanup-backups
help:
@echo "Usage: make <target> [ENV_FILE=.env BACKUP_ROOT=backups RETENTION_DAYS=14]"
@echo ""
@echo "Targets:"
@echo " db-dump Create a gzip-compressed MySQL dump from .env settings"
@echo " files-backup Archive runtime files such as uploads/, map_config.json, and .env"
@echo " full-backup Run both db-dump and files-backup"
@echo " predeploy-backup Alias for the backup step executed before production deploy"
@echo " cleanup-backups Delete backup files older than RETENTION_DAYS"
db-dump:
@ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" db
files-backup:
@ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" files
full-backup:
@ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" full
predeploy-backup:
@ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" full
cleanup-backups:
@BACKUP_ROOT="$(BACKUP_ROOT)" RETENTION_DAYS="$(RETENTION_DAYS)" sh "$(BACKUP_SCRIPT)" cleanup

View File

@@ -1,429 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PC 사양 적정성 분석 기획서 (GPU 반영)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary: #4F46E5;
--primary-light: #EEF2FF;
--secondary: #10B981;
--secondary-light: #D1FAE5;
--danger: #EF4444;
--danger-light: #FEE2E2;
--warning: #F59E0B;
--warning-light: #FEF3C7;
--purple: #7C3AED;
--purple-light: #EDE9FE;
--text-dark: #0F172A;
--text-body: #334155;
--text-muted: #64748B;
--border: #E2E8F0;
--bg-light: #F8FAFC;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Outfit', 'Noto Sans KR', sans-serif;
color: var(--text-body);
background: #fff;
letter-spacing: -0.02em;
line-height: 1.7;
}
.page { max-width: 980px; margin: 0 auto; padding: 3rem 2rem; }
/* ─ Header ─ */
.doc-header { border-bottom: 3px solid var(--text-dark); padding-bottom: 1.75rem; margin-bottom: 3rem; }
.doc-label {
display: inline-block; font-size: 0.75rem; font-weight: 700; color: var(--primary);
background: var(--primary-light); padding: 0.25rem 0.75rem; border-radius: 99px;
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.75rem;
}
.version-badge {
display: inline-block; font-size: 0.7rem; font-weight: 700; color: var(--secondary);
background: var(--secondary-light); padding: 0.2rem 0.6rem; border-radius: 99px;
margin-left: 0.5rem; vertical-align: middle;
}
.doc-header h1 { font-size: 2rem; font-weight: 900; color: var(--text-dark); line-height: 1.25; margin-bottom: 1rem; }
.meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; margin-top: 1rem; }
.meta-item { background: var(--bg-light); border-radius: 8px; padding: 0.65rem 1rem; font-size: 0.83rem; }
.meta-item .label { color: var(--text-muted); display: block; font-size: 0.75rem; }
.meta-item .val { font-weight: 700; color: var(--text-dark); font-size: 0.9rem; }
/* ─ Sections ─ */
section { margin-bottom: 3.5rem; }
h2 {
font-size: 1.3rem; font-weight: 800; color: var(--text-dark);
padding-bottom: 0.5rem; border-bottom: 2px solid var(--border);
margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.6rem;
}
h2 .num {
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; background: var(--primary); color: #fff;
border-radius: 50%; font-size: 0.75rem; font-weight: 800; flex-shrink: 0;
}
h3 { font-size: 1.05rem; font-weight: 700; color: var(--text-dark); margin: 1.75rem 0 0.75rem; }
p { margin-bottom: 1rem; color: var(--text-body); font-size: 0.97rem; }
/* ─ Boxes ─ */
.box { border-radius: 10px; padding: 1.25rem 1.5rem; margin: 1.25rem 0; font-size: 0.93rem; }
.box-blue { background: var(--primary-light); border-left: 4px solid var(--primary); }
.box-green { background: var(--secondary-light); border-left: 4px solid var(--secondary); }
.box-yellow { background: var(--warning-light); border-left: 4px solid var(--warning); }
.box-red { background: var(--danger-light); border-left: 4px solid var(--danger); }
.box-purple { background: var(--purple-light); border-left: 4px solid var(--purple); }
.box-title { font-weight: 700; color: var(--text-dark); margin-bottom: 0.5rem; font-size: 0.95rem; }
/* ─ Score formula block ─ */
.formula {
background: #1E293B; color: #E2E8F0; border-radius: 8px;
padding: 1rem 1.25rem; font-family: 'Courier New', monospace;
font-size: 0.87rem; margin: 1rem 0; overflow-x: auto; line-height: 2;
}
.formula .comment { color: #64748B; }
.formula .key { color: #93C5FD; }
.formula .val { color: #6EE7B7; }
.formula .warn { color: #FCD34D; }
/* ─ Three-col score grid ─ */
.score-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.1rem; margin: 1.5rem 0; }
@media(max-width: 700px) { .score-grid-3 { grid-template-columns: 1fr; } }
.score-card { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
.score-card-header {
background: var(--bg-light); padding: 0.65rem 1rem;
font-weight: 700; font-size: 0.88rem; color: var(--text-dark);
border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 0.5rem;
}
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--primary); }
.dot-green { background: var(--secondary); }
.dot-purple { background: var(--purple); }
/* ─ Tables ─ */
.tbl-wrap { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin: 1.25rem 0; }
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
th { background: var(--bg-light); padding: 0.65rem 1rem; font-weight: 700; color: var(--text-dark); border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; }
td { padding: 0.65rem 1rem; border-bottom: 1px solid var(--border); color: var(--text-body); vertical-align: top; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--bg-light); }
/* ─ Badges ─ */
.badge { display: inline-block; padding: 0.2rem 0.55rem; border-radius: 4px; font-size: 0.75rem; font-weight: 700; white-space: nowrap; }
.b-primary { color: var(--primary); background: var(--primary-light); }
.b-green { color: #065F46; background: var(--secondary-light); }
.b-red { color: #991B1B; background: var(--danger-light); }
.b-yellow { color: #92400E; background: var(--warning-light); }
.b-purple { color: #5B21B6; background: var(--purple-light); }
/* ─ Flow ─ */
.flow { display: flex; align-items: center; flex-wrap: wrap; gap: 0; margin: 1.5rem 0; }
.flow-step { background: var(--primary-light); color: var(--primary); font-weight: 700; font-size: 0.83rem; padding: 0.55rem 0.9rem; border-radius: 8px; text-align: center; }
.flow-step.gpu { background: var(--purple-light); color: var(--purple); }
.flow-arrow { font-size: 1.1rem; color: var(--text-muted); padding: 0 0.4rem; }
/* ─ GPU tier table highlight ─ */
.tier-S td:first-child { font-weight: 800; color: #DC2626; }
.tier-A td:first-child { font-weight: 700; color: var(--primary); }
.tier-B td:first-child { font-weight: 700; color: var(--secondary); }
.tier-C td:first-child { color: var(--warning); font-weight: 600; }
.tier-D td:first-child { color: var(--text-muted); }
footer { border-top: 1px solid var(--border); margin-top: 4rem; padding-top: 1.5rem; text-align: center; font-size: 0.8rem; color: var(--text-muted); }
</style>
</head>
<body>
<div class="page">
<!-- HEADER -->
<header class="doc-header">
<div class="doc-label">기능 명세서 <span class="version-badge">v3.0 — 100점 감점제 반영</span></div>
<h1>PC 사양 적정성 분석 기획서<br>
<span style="font-size:1.05rem;font-weight:500;color:var(--text-muted);">
100점 만점 감점 방식 · 성능 감점 기준 · 실제 업무 효율성 평가 (CPU / RAM / GPU / 연식)
</span>
</h1>
<div class="meta-grid">
<div class="meta-item"><span class="label">분석 지표</span><span class="val">CPU + RAM + GPU + 연식 (감점법)</span></div>
<div class="meta-item"><span class="label">최대 점수</span><span class="val">100점 (만점)</span></div>
<div class="meta-item"><span class="label">적정성 판별 기준</span><span class="val">직무별 목표 사양 대비 편차</span></div>
<div class="meta-item"><span class="label">최종 수정일</span><span class="val">2026. 05. 31</span></div>
</div>
</header>
<!-- 1. 개요 -->
<section>
<h2><span class="num">1</span>개요 — 100점 만점 감점형 성능 점수 체계</h2>
<p>
v3.0부터 PC 사양 점수는 <strong>100점 만점 기준 감점제</strong>로 산출됩니다.
누적 합산 방식 대신, 최상급 부품 조합을 100점 만점으로 고정하고 사양이 저하되거나 연식이 노후화됨에 따라
<strong>성능 및 효율성 하락 폭을 감점</strong>하는 방식입니다. 이는 실제 업무 환경에서 PC 노후도에 따른
체감 생산성 저하를 훨씬 직관적이고 현실적으로 드러냅니다.
</p>
<div class="flow">
<div class="flow-step">① 기본 100점 만점</div>
<div class="flow-arrow"></div>
<div class="flow-step">② CPU 등급/세대 감점</div>
<div class="flow-arrow"></div>
<div class="flow-step">③ RAM 용량 감점</div>
<div class="flow-arrow"></div>
<div class="flow-step gpu">④ GPU 등급 감점</div>
<div class="flow-arrow"></div>
<div class="flow-step">⑤ 연식 노후 감점</div>
<div class="flow-arrow"></div>
<div class="flow-step">⑥ 최종 실질 성능 점수</div>
</div>
<div class="formula">
<span class="comment">// ─── 최종 PC 사양 점수 (100점 만점, 최소 10점 보존) ───</span>
<span class="key">totalScore</span> = max(10, 100 - (<span class="val">cpuDeduction</span> + <span class="val">genDeduction</span> + <span class="val">ramDeduction</span> + <span class="val">gpuDeduction</span> + <span class="val">ageDeduction</span>))
</div>
</section>
<!-- 2. CPU 감점 룰 -->
<section>
<h2><span class="num">2</span>CPU 사양 감점 기준</h2>
<p>CPU 감점은 <strong>등급 감점(최대 -30점)</strong><strong>세대 노후 감점(최대 -15점)</strong>의 합산입니다.</p>
<div class="formula">
<span class="comment">// [CPU 등급 감점]</span>
i9 / Ryzen 9 → <span class="val">0점 감점</span>
i7 / Ryzen 7 → <span class="val">-5점 감점</span>
i5 / Ryzen 5 → <span class="val">-15점 감점</span>
i3 / Ryzen 3 → <span class="val">-25점 감점</span>
기타 → <span class="val">-30점 감점</span>
<span class="comment">// [CPU 세대 노후 감점]</span>
최신 세대 (Intel 12~14세대, Ryzen 5000~7000시리즈 이상) → <span class="val">0점 감점</span>
과도기 세대 (Intel 10~11세대, Ryzen 3000시리즈) → <span class="val">-5점 감점</span>
구형 세대 (Intel 8~9세대, Ryzen 1000~2000시리즈) → <span class="val">-10점 감점</span>
노후 세대 (Intel 7세대 이하, 구형 AMD) → <span class="val">-15점 감점</span>
</div>
<h3>CPU 조합별 감점 예시</h3>
<div class="tbl-wrap">
<table>
<thead><tr><th>모델</th><th>세대 구분</th><th>등급감점</th><th>세대감점</th><th>CPU 감점 합계</th></tr></thead>
<tbody>
<tr><td>i9-13900K</td><td>최신 세대</td><td>0</td><td>0</td><td><strong>0점 (감점 없음)</strong></td></tr>
<tr><td>i7-14700K</td><td>최신 세대</td><td>-5</td><td>0</td><td><strong>-5점</strong></td></tr>
<tr><td>i7-1360P</td><td>최신 세대 (노트북)</td><td>-5</td><td>0</td><td><strong>-5점</strong></td></tr>
<tr><td>i5-12400</td><td>최신 세대</td><td>-15</td><td>0</td><td><strong>-15점</strong></td></tr>
<tr><td>i7-9700</td><td>구형 세대</td><td>-5</td><td>-10</td><td><strong>-15점</strong></td></tr>
<tr><td>i5-8500</td><td>구형 세대</td><td>-15</td><td>-10</td><td><strong>-25점</strong></td></tr>
<tr><td>i7-7700</td><td>노후 세대</td><td>-5</td><td>-15</td><td><strong>-20점</strong></td></tr>
</tbody>
</table>
</div>
</section>
<!-- 3. RAM 감점 룰 -->
<section>
<h2><span class="num">3</span>RAM 용량 감점 기준</h2>
<p>메모리 용량 부족에 따른 멀티태스킹 제약 및 병목 현상을 반영해 <strong>최대 -25점</strong>까지 감점합니다.</p>
<div class="tbl-wrap">
<table>
<thead><tr><th>RAM 용량</th><th>감점 점수</th><th>영향도</th><th>평가</th></tr></thead>
<tbody>
<tr><td>32GB 이상</td><td><strong>0점 (감점 없음)</strong></td><td>대용량 3D 및 개발 작업 원활</td><td><span class="badge b-green">최적</span></td></tr>
<tr><td>16GB</td><td><strong>-10점 감점</strong></td><td>일반 사무용 및 가벼운 멀티태스킹 적합</td><td><span class="badge b-primary">보통</span></td></tr>
<tr><td>8GB</td><td><strong>-20점 감점</strong></td><td>브라우저 탭 다수 실행 시 물리 메모리 부족</td><td><span class="badge b-yellow">주의</span></td></tr>
<tr><td>8GB 미만</td><td><strong>-25점 감점</strong></td><td>기본 OS 구동 외 심각한 메모리 병목</td><td><span class="badge b-red">부족</span></td></tr>
</tbody>
</table>
</div>
</section>
<!-- 4. GPU 감점 룰 -->
<section>
<h2><span class="num">4</span>GPU 성능 감점 기준</h2>
<p>
3D 렌더링 및 고급 연산 처리 능력을 기준으로 외장 및 내장 GPU를 분류해 <strong>최대 -25점</strong>까지 감점합니다.
GPU 정보가 감지되지 않거나 없는 경우 기본적으로 내장 그래픽 수준인 -25점을 감점합니다.
</p>
<div class="tbl-wrap">
<table>
<thead><tr><th>등급</th><th>제품군 구분</th><th>대표 모델</th><th>감점 점수</th><th>적합 작업</th></tr></thead>
<tbody>
<tr class="tier-S"><td>S</td><td>최상위 외장 GPU</td><td>RTX 4070~4090, RTX A4000~A6000</td><td><strong>0점 (감점 없음)</strong></td><td>3D 그래픽, AI 연산, VR</td></tr>
<tr class="tier-A"><td>A</td><td>메인스트림 외장 GPU</td><td>RTX 3060~3070, RTX 2060, RTX A2000</td><td><strong>-5점 감점</strong></td><td>중급 개발, CAD 설계</td></tr>
<tr class="tier-B"><td>B</td><td>엔트리 외장 GPU</td><td>GTX 1660, GTX 1060, RX 6600</td><td><strong>-15점 감점</strong></td><td>기본 CAD, 그래픽 보조</td></tr>
<tr class="tier-C"><td>C</td><td>내장 그래픽 및 기타</td><td>Intel Iris Xe, UHD Graphics, Vega, GPU 없음</td><td><strong>-25점 감점</strong></td><td>오피스 사무, 문서 작업</td></tr>
</tbody>
</table>
</div>
</section>
<!-- 5. 종합 점수 감점 사례 -->
<section>
<h2><span class="num">5</span>감점법 종합 점수 계산 실사례</h2>
<div class="tbl-wrap">
<table>
<thead>
<tr><th>모델명</th><th>CPU 사양 (감점)</th><th>RAM 사양 (감점)</th><th>GPU 사양 (감점)</th><th>연식 (감점)</th><th>감점 총합</th><th>최종 점수</th></tr>
</thead>
<tbody>
<tr>
<td>HP ZBook Fury 16</td><td>Ryzen 9 7900X (0)</td><td>64GB (0)</td><td>NVIDIA RTX A2000 (-5)</td><td>2년차 (-6)</td><td>-11</td><td><strong>89점</strong></td>
</tr>
<tr>
<td>Dell Precision 5680</td><td>i9-13900K (0)</td><td>64GB (0)</td><td>NVIDIA RTX 4070 (0)</td><td>2년차 (-6)</td><td>-6</td><td><strong>94점</strong></td>
</tr>
<tr>
<td>LG Gram 17 Pro</td><td>i7-14700K (-5)</td><td>32GB (0)</td><td>NVIDIA RTX 4060 (-5)</td><td>1년차 (-3)</td><td>-13</td><td><strong>87점</strong></td>
</tr>
<tr>
<td>LG Gram 16</td><td>i7-1360P (-5)</td><td>16GB (-10)</td><td>Intel Iris Xe (-25)</td><td>3년차 (-9)</td><td>-49</td><td><strong>51점</strong></td>
</tr>
<tr>
<td>Samsung Galaxy Book 3</td><td>i5-1340P (-15)</td><td>16GB (-10)</td><td>Intel Iris Xe (-25)</td><td>3년차 (-9)</td><td>-59</td><td><strong>41점</strong></td>
</tr>
<tr>
<td>HP EliteBook 840</td><td>Ryzen 5 5600X (-15)</td><td>16GB (-10)</td><td>AMD Radeon Vega (-25)</td><td>4년차 (-12)</td><td>-62</td><td><strong>38점</strong></td>
</tr>
<tr>
<td>HP ProDesk 400 G5</td><td>i3-8100 (-35)</td><td>8GB (-20)</td><td>Intel UHD 630 (-25)</td><td>5년 이상 (-15)</td><td>-95</td><td><strong>10점(보존)</strong></td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 6. 직무별 평균 및 권장 점수 -->
<section>
<h2><span class="num">6</span>직무별 평균 및 권장 점수 기준 (100점 만점 감점형)</h2>
<p>100점 만점 감점형 점수 체계를 실제 PC 데이터에 대입하여 산출된 각 직무별 평균 및 권장 목표 점수 기준선입니다.</p>
<div class="tbl-wrap">
<table>
<thead>
<tr><th>정렬</th><th>직무</th><th>실제 데이터 평균 (감점 반영)</th><th>기본 권장 점수 (목표)</th><th>규칙</th></tr>
</thead>
<tbody>
<tr><td>1</td><td><strong>AI 개발자</strong></td><td>88.0점</td><td>95점</td><td><span class="badge b-purple">최고</span></td></tr>
<tr><td>2</td><td><strong>편집 디자이너</strong></td><td>80.2점</td><td>75점</td><td><span class="badge b-purple">최고</span></td></tr>
<tr><td>3</td><td><strong>3D 디자이너</strong></td><td>78.4점</td><td>90점</td><td><span class="badge b-purple">최고</span></td></tr>
<tr><td>4</td><td><strong>UXUI 디자이너</strong></td><td>72.7점</td><td>70점</td><td><span class="badge b-primary">고성능</span></td></tr>
<tr><td>5</td><td><strong>3D 개발자</strong></td><td>67.8점</td><td>90점</td><td><span class="badge b-purple">최고</span></td></tr>
<tr><td>6</td><td><strong>프로그램 개발자</strong></td><td>67.3점</td><td>80점</td><td><span class="badge b-primary">고성능</span></td></tr>
<tr><td>7</td><td><strong>BIM모델러</strong></td><td>62.1점</td><td>75점</td><td><span class="badge b-purple">최고</span></td></tr>
<tr><td>8</td><td><strong>엔지니어</strong></td><td>42.9점</td><td>60점</td><td><span class="badge b-primary">고성능</span></td></tr>
<tr><td>9</td><td><strong>웹 개발자</strong></td><td>39.2점</td><td>75점</td><td><span class="badge b-primary">고성능</span></td></tr>
<tr><td>10</td><td><strong>기획자</strong></td><td>38.6점</td><td>50점</td><td><span class="badge b-green">중간</span></td></tr>
<tr><td>11</td><td><strong>감리원</strong></td><td>-</td><td>40점</td><td><span class="badge b-yellow">기본</span></td></tr>
</tbody>
</table>
</div>
<div class="box box-blue">
<div class="box-title">📌 대소 관계 조건 충족 확인</div>
AI 개발자(88.0) &gt; 편집 디자이너(80.2) &gt; 3D 디자이너(78.4) &gt; UXUI 디자이너(72.7) &gt; 3D 개발자(67.8) &gt; 프로그램 개발자(67.3) &gt; BIM모델러(62.1) &gt; 엔지니어(42.9) &gt; 웹 개발자(39.2) &gt; 기획자(38.6) ✅
</div>
</section>
<!-- 7. 적정성 판별 기준 -->
<section>
<h2><span class="num">7</span>적정성 판별 기준</h2>
<p>직무 내 실제 평균 점수를 기준으로 편차율을 산출하여 3단계로 판별합니다.</p>
<div class="formula">
<span class="key">avgScore</span> = <span class="val">해당 직무 소속 PC 점수들의 산술 평균</span>
IF <span class="val">개인 실질 점수 &lt; avgScore × 0.80</span><span class="key">"사양 부족"</span> (직무 평균 20% 이상 미달)
IF <span class="val">개인 실질 점수 &gt; avgScore × 1.30</span><span class="key">"오버스펙"</span> (직무 평균 30% 이상 초과)
ELSE → <span class="key">"적정"</span>
</div>
<div class="tbl-wrap">
<table>
<thead><tr><th>판별 결과</th><th>조건</th><th>권장 조치</th></tr></thead>
<tbody>
<tr><td><span class="badge b-red">사양 부족</span></td><td>실질 점수 &lt; 직무 평균 × 0.8</td><td>교체 또는 성능 업그레이드 우선 검토</td></tr>
<tr><td><span class="badge b-green">적정</span></td><td>직무 평균 × 0.8 ≤ 실질 점수 ≤ 직무 평균 × 1.3</td><td>현행 업무 효율 유지</td></tr>
<tr><td><span class="badge b-yellow">오버스펙</span></td><td>실질 점수 &gt; 직무 평균 × 1.3</td><td>과스펙 장비 회수 또는 필요 부서 재배치</td></tr>
</tbody>
</table>
</div>
</section>
<!-- 8. 신뢰도 검토 -->
<section>
<h2><span class="num">8</span>점수 신뢰도 및 한계 분석</h2>
<h3>✅ 신뢰 가능한 부분</h3>
<div class="box box-green">
<ul style="padding-left:1.25rem;margin:0;line-height:2.2;">
<li><strong>3요소 합산으로 실제 성능 근접도 향상</strong>: CPU·RAM·GPU를 모두 반영함으로써 단순 CPU 점수 대비 실체감 성능과의 상관관계가 크게 개선되었습니다.</li>
<li><strong>GPU 티어 방향성 일치</strong>: RTX 4090 &gt; 4080 &gt; 4070 … 순의 점수 순서는 실제 벤치마크(3DMark, PassMark GPU)와 일치합니다.</li>
<li><strong>내장/외장 구분 명확</strong>: 내장 그래픽(5~15점)과 독립 GPU(18점~)의 점수 구간이 명확히 분리되어 사양 격차를 직관적으로 반영합니다.</li>
<li><strong>직무별 상대 비교 합리성 유지</strong>: GPU 점수 추가 후에도 직무 내 평균 기준 편차율 판별 방식이 그대로 유지됩니다.</li>
</ul>
</div>
<h3>⚠️ 여전히 남아있는 한계점</h3>
<div class="tbl-wrap">
<table>
<thead><tr><th>한계 항목</th><th>내용</th><th>영향도</th></tr></thead>
<tbody>
<tr>
<td><strong>노트북 TDP 미반영</strong></td>
<td>i7-1360P (노트북 28W)와 i7-13700K (데스크탑 125W)는 같은 세대지만 실제 성능 차이가 큽니다. 현재는 동일 점수가 부여됩니다.</td>
<td><span class="badge b-yellow">중간</span></td>
</tr>
<tr>
<td><strong>SSD 유형 미반영</strong></td>
<td>NVMe SSD와 HDD의 체감 속도 차이는 크지만 점수에 포함되지 않습니다.</td>
<td><span class="badge b-yellow">중간</span></td>
</tr>
<tr>
<td><strong>GPU 세부 파생 모델 한계</strong></td>
<td>RTX 4060 Laptop과 RTX 4060 Desktop은 성능 차이가 있으나 동일 점수(50점)를 받습니다.</td>
<td><span class="badge b-yellow">중간</span></td>
</tr>
<tr>
<td><strong>GPU 세대 보정 미적용</strong></td>
<td>CPU와 달리 GPU는 세대 보정 없이 모델명 매핑 방식만 사용됩니다. 향후 세대별 보정을 검토할 수 있습니다.</td>
<td><span class="badge b-primary">낮음</span></td>
</tr>
<tr>
<td><strong>실측 벤치마크 미연동</strong></td>
<td>3DMark / PassMark GPU 실측값이 아닌 모델명 파싱 추정치입니다.</td>
<td><span class="badge b-yellow">중간</span></td>
</tr>
</tbody>
</table>
</div>
<div class="box box-blue">
<div class="box-title">💡 종합 신뢰도 평가</div>
GPU 점수 반영 후 <strong>특히 디자이너·개발자와 같은 그래픽 집약적 직무의 적정성 판별 정확도가 대폭 향상</strong>되었습니다.
다만 노트북 TDP, SSD 유형 등 추가 변수를 향후 보완하면 신뢰도를 더 끌어올릴 수 있습니다.
현 시점에서 본 점수 체계는 <strong>"절대적 성능 수치"가 아닌 "조직 내 직무별 상대 비교 도구"</strong>로 활용하는 것이 가장 적합합니다.
</div>
</section>
<!-- 9. 개선 로드맵 -->
<section>
<h2><span class="num">9</span>향후 개선 로드맵</h2>
<div class="tbl-wrap">
<table>
<thead><tr><th>우선순위</th><th>항목</th><th>기대 효과</th><th>난이도</th></tr></thead>
<tbody>
<tr><td><span class="badge b-green">완료</span></td><td>GPU 점수 반영 (v2.0)</td><td>그래픽 직무 신뢰도 대폭 향상</td><td></td></tr>
<tr><td><span class="badge b-yellow">권장</span></td><td>SSD 유형별 점수 추가 (NVMe/SATA/HDD)</td><td>실체감 체감 속도 반영</td><td></td></tr>
<tr><td><span class="badge b-yellow">권장</span></td><td>노트북/데스크탑 TDP 보정</td><td>모바일 CPU 과대평가 방지</td><td></td></tr>
<tr><td><span class="badge b-primary">선택</span></td><td>PassMark / 3DMark 실측 DB 내장 연동</td><td>추정치 → 실측값 전환</td><td></td></tr>
<tr><td><span class="badge b-primary">선택</span></td><td>직무별 항목 가중치 커스터마이징</td><td>조직 특성 맞춤 정밀 점수화</td><td></td></tr>
<tr><td><span class="badge b-primary">선택</span></td><td>RMM 에이전트 실시간 자원 점유율 연동</td><td>실사용 기반 교체 우선순위 추천</td><td></td></tr>
</tbody>
</table>
</div>
</section>
<footer>
<p>HM ITAM — PC 사양 적정성 분석 기획서 v2.0 (GPU 반영) &nbsp;·&nbsp; 2026. 05. 28</p>
<p style="margin-top:0.25rem;">내부 검토용 문서입니다. 무단 외부 배포를 금합니다.</p>
</footer>
</div>
</body>
</html>

102
README.md
View File

@@ -1,56 +1,46 @@
# 🛠️ 개발 및 관리 규칙 (Strict Development Rules) # 🛠️ 개발 및 관리 규칙 (Strict Development Rules)
1. **언어 설정**: 영어로 생각하되, 모든 답변은 **한국어**로 작성한다. 1. **언어 설정**: 영어로 생각하되, 모든 답변은 **한국어**로 작성한다.
2. **임의 수정 절대 금지 (Zero-Arbitrary Change)**: 2. **임의 수정 절대 금지 (Zero-Arbitrary Change)**:
- 사용자가 명시적으로 지시한 부분 외에는 **단 한 줄의 코드도, 그 어떤 파일도 임의로 수정, 정리, 리팩토링하지 않는다.** - 사용자가 명시적으로 지시한 부분 외에는 **단 한 줄의 코드도, 그 어떤 파일도 임의로 수정, 정리, 리팩토링하지 않는다.**
- 지시받지 않은 다른 파트의 코드는 절대 건드리지 않으며, 영향 범위가 요청 범위를 벗어나지 않도록 '외과 수술식(Surgical) 수정'을 원칙으로 한다. - 지시받지 않은 다른 파트의 코드는 절대 건드리지 않으며, 영향 범위가 요청 범위를 벗어나지 않도록 '외과 수술식(Surgical) 수정'을 원칙으로 한다.
3. **개선 작업 절차 (Test-First Approach)**: 3. **개선 작업 절차 (Test-First Approach)**:
- 사용자가 개선(Refactoring, Optimization 등)을 지시한 경우, **수정 전 현재 시스템이 정상적으로 잘 작동하는지 먼저 전수 확인**한다. - 사용자가 개선(Refactoring, Optimization 등)을 지시한 경우, **수정 전 현재 시스템이 정상적으로 잘 작동하는지 먼저 전수 확인**한다.
- 기존 동작 방식과 성능을 기준(Baseline)으로 삼고, 수정 후에도 **기존의 모든 기능이 무결하게 유지되는지 반드시 테스트하여 입증**한다. - 기존 동작 방식과 성능을 기준(Baseline)으로 삼고, 수정 후에도 **기존의 모든 기능이 무결하게 유지되는지 반드시 테스트하여 입증**한다.
- 검증 결과를 바탕으로 "무엇을, 왜, 어떻게" 바꿀지 상세 보고 후, 사용자로부터 **'진행시켜'** 승인을 얻은 뒤에만 집행한다. - 검증 결과를 바탕으로 "무엇을, 왜, 어떻게" 바꿀지 상세 보고 후, 사용자로부터 **'진행시켜'** 승인을 얻은 뒤에만 집행한다.
4. **선보고 후승인**: 모든 기능 수정 및 코드 변경 전에는 예상 방안을 먼저 보고하고 승인 절차를 거친다. 4. **선보고 후승인**: 모든 기능 수정 및 코드 변경 전에는 예상 방안을 먼저 보고하고 승인 절차를 거친다.
5. **DB 삭제 및 초기화 절대 엄금 (Strict DB Deletion Policy)**:
--- - 어떠한 경우에도 `DELETE`, `DROP`, `TRUNCATE` 등 데이터를 삭제하거나 테이블을 초기화하는 작업은 사전에 사용자에게 상세 사유를 보고하고 **명시적 승인**을 얻은 후에만 시행한다.
- 기존 데이터의 가치를 최우선으로 하며, 작업 전 백업 여부를 반드시 확인한다.
### 🚀 서버 구동 및 외부 접속 규칙 (Server Run & External Access) 6. **REDGREENRefactor 개발 원칙**:
- 모든 기능 개발과 버그 수정은 **RED → GREEN → Refactor** 순서로 진행한다.
1. **포트 고정**: 개발 서버는 반드시 **8080** 포트를 사용한다. (`vite.config.ts` 설정 준수) - **RED**: 요구사항을 명확히 표현하는 테스트를 먼저 작성하고, 해당 테스트가 기능 미구현 또는 결함으로 인해 실패하는지 확인한다.
2. **외부 접속 허용 (Host)**: 사무실 내 타 직원이 접속할 수 있도록 `--host` 모드로 구동한다. - **GREEN**: 실패한 테스트를 통과시키는 데 필요한 최소한의 코드만 구현하며, 불필요한 기능 추가나 구조 변경을 하지 않는다.
3. **구동 명령어**: - **Refactor**: 관련 테스트와 기존 테스트가 모두 통과하는 상태에서만 중복 제거, 명칭 개선, 책임 분리 등 코드 구조를 개선하며 동작은 변경하지 않는다.
```bash - 각 단계가 끝날 때마다 관련 테스트와 기존 기능의 회귀 여부를 검증한다.
npm run dev - 테스트 작성이 현실적으로 불가능한 경우에는 그 사유와 대체 검증 방법을 먼저 보고하고 승인을 받은 후 진행한다.
``` - 본 원칙을 적용할 때에도 기존의 **선보고 후승인****외과 수술식 수정** 규칙을 준수한다.
* 해당 명령어 실행 시 `0.0.0.0` 또는 `Network: http://[내-IP]:8080/` 경로로 타인 접속이 가능하다.
4. **IP 확인 방법**: ---
* Windows: `ipconfig` 명령어로 'IPv4 주소' 확인 후 공유.
### 🚀 서버 구동 및 외부 접속 규칙 (Server Run & External Access)
---
1. **포트 고정**: 개발 서버는 반드시 **8080** 포트를 사용한다. (`vite.config.ts` 설정 준수)
### 🎨 ITAM 시스템 디자인 가이드 (Design Guide) 2. **외부 접속 허용 (Host)**: 사무실 내 타 직원이 접속할 수 있도록 `--host` 모드로 구동한다.
3. **구동 명령어**:
1. **디자인 철학 (Design Philosophy)** ```bash
* **Minimalist & Border-based**: 불필요한 박스(Card) 사용을 최소화하고, 정보의 구분은 간결한 라인(Border/Divider)을 활용하여 시각적 피로도를 낮춥니다. npm run dev
* **Professional Achromatic**: 무채색(Black, White, Grey)을 기본으로 하여 정돈된 업무 환경을 제공합니다. ```
* **Green Accent**: 블루 대신 짙은 그린(`#1E5149`)을 포인트 컬러로 사용하여 차분한 전문성을 강조합니다. * 해당 명령어 실행 시 `0.0.0.0` 또는 `Network: http://[내-IP]:8080/` 경로로 타인 접속이 가능하다.
4. **IP 확인 방법**:
2. **타이포그래피 (Typography)** * Windows: `ipconfig` 명령어로 'IPv4 주소' 확인 후 공유.
* **Font Family**: `Pretendard` (전역 적용)
* **Letter Spacing**: `-0.02em` (약 -2%) 적용. 자간을 좁게 설정하여 밀도 있고 세련된 가독성을 확보합니다. ---
* **Weights**: 400(Regular), 500(Medium), 600(SemiBold), 700(Bold).
### 🎨 ITAM 시스템 디자인 가이드 (Design Guide)
3. **컬러 팔레트 (Color Palette)**
* **Point Color**: `#1E5149` (Deep Green) - 강조, 활성화 상태, 주요 액션 버튼. 디자인 일관성 및 시각적 원칙에 관한 상세 내용은 아래 문서를 참조하십시오.
* **Text**: Main(`#111827` - Near Black), Muted(`#6B7280` - Grey).
* **Border/Divider**: `#E5E7EB` (Light Grey) - 정보 구분을 위한 얇은 실선. 👉 **[디자인 가이드 바로가기 (design_rule.md)](./design_rule.md)**
* **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열 그리드 시스템을 권장하며, 하단 우측에 액션 버튼(닫기, 저장 등)을 배치합니다.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,211 +0,0 @@
('D:\\이태훈\\22전산자산조사\\ITAM\\dist\\pc_agent.exe',
True,
False,
False,
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico',
None,
False,
False,
b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<assembly xmlns='
b'"urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">\n <trustInfo x'
b'mlns="urn:schemas-microsoft-com:asm.v3">\n <security>\n <requested'
b'Privileges>\n <requestedExecutionLevel level="asInvoker" uiAccess='
b'"false"/>\n </requestedPrivileges>\n </security>\n </trustInfo>\n '
b'<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">\n <'
b'application>\n <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f'
b'0}"/>\n <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>\n '
b' <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>\n <s'
b'upportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>\n <supporte'
b'dOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>\n </application>\n <'
b'/compatibility>\n <application xmlns="urn:schemas-microsoft-com:asm.v3">'
b'\n <windowsSettings>\n <longPathAware xmlns="http://schemas.micros'
b'oft.com/SMI/2016/WindowsSettings">true</longPathAware>\n </windowsSett'
b'ings>\n </application>\n <dependency>\n <dependentAssembly>\n <ass'
b'emblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version='
b'"6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" langua'
b'ge="*"/>\n </dependentAssembly>\n </dependency>\n</assembly>',
True,
False,
None,
None,
None,
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\pc_agent.pkg',
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz', 'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\PYZ-00.pyz', 'PYZ'),
('struct',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth_inspect',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
'PYSOURCE'),
('pyi_rth_cryptography_openssl',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_cryptography_openssl.py',
'PYSOURCE'),
('pyi_rth_pywintypes',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_pywintypes.py',
'PYSOURCE'),
('pyi_rth_pythoncom',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_pythoncom.py',
'PYSOURCE'),
('pc_agent', 'D:\\이태훈\\22전산자산조사\\ITAM\\pc_agent.py', 'PYSOURCE'),
('python312.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll',
'BINARY'),
('pywin32_system32\\pywintypes312.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\pywin32_system32\\pywintypes312.dll',
'BINARY'),
('pywin32_system32\\pythoncom312.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\pywin32_system32\\pythoncom312.dll',
'BINARY'),
('select.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\select.pyd',
'EXTENSION'),
('_multiprocessing.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_multiprocessing.pyd',
'EXTENSION'),
('pyexpat.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\pyexpat.pyd',
'EXTENSION'),
('_ssl.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_ssl.pyd',
'EXTENSION'),
('_hashlib.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_hashlib.pyd',
'EXTENSION'),
('unicodedata.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\unicodedata.pyd',
'EXTENSION'),
('_decimal.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_decimal.pyd',
'EXTENSION'),
('_lzma.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_lzma.pyd',
'EXTENSION'),
('_bz2.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_bz2.pyd',
'EXTENSION'),
('_ctypes.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_ctypes.pyd',
'EXTENSION'),
('_queue.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_queue.pyd',
'EXTENSION'),
('_wmi.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_wmi.pyd',
'EXTENSION'),
('_socket.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_socket.pyd',
'EXTENSION'),
('_overlapped.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_overlapped.pyd',
'EXTENSION'),
('_asyncio.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_asyncio.pyd',
'EXTENSION'),
('_cffi_backend.cp312-win_amd64.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_cffi_backend.cp312-win_amd64.pyd',
'EXTENSION'),
('cryptography\\hazmat\\bindings\\_rust.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography\\hazmat\\bindings\\_rust.pyd',
'EXTENSION'),
('charset_normalizer\\md__mypyc.cp312-win_amd64.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\charset_normalizer\\md__mypyc.cp312-win_amd64.pyd',
'EXTENSION'),
('charset_normalizer\\md.cp312-win_amd64.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\charset_normalizer\\md.cp312-win_amd64.pyd',
'EXTENSION'),
('win32\\_win32sysloader.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\_win32sysloader.pyd',
'EXTENSION'),
('win32\\win32api.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\win32api.pyd',
'EXTENSION'),
('Pythonwin\\win32ui.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\Pythonwin\\win32ui.pyd',
'EXTENSION'),
('win32\\win32event.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\win32event.pyd',
'EXTENSION'),
('win32\\win32trace.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\win32trace.pyd',
'EXTENSION'),
('VCRUNTIME140.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\VCRUNTIME140.dll',
'BINARY'),
('VCRUNTIME140_1.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\VCRUNTIME140_1.dll',
'BINARY'),
('libssl-3.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libssl-3.dll',
'BINARY'),
('libcrypto-3.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libcrypto-3.dll',
'BINARY'),
('libffi-8.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libffi-8.dll',
'BINARY'),
('python3.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\python3.dll',
'BINARY'),
('Pythonwin\\mfc140u.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\Pythonwin\\mfc140u.dll',
'BINARY'),
('certifi\\cacert.pem',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\certifi\\cacert.pem',
'DATA'),
('certifi\\py.typed',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\certifi\\py.typed',
'DATA'),
('cryptography-45.0.2.dist-info\\licenses\\LICENSE.BSD',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\licenses\\LICENSE.BSD',
'DATA'),
('cryptography-45.0.2.dist-info\\licenses\\LICENSE',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\licenses\\LICENSE',
'DATA'),
('cryptography-45.0.2.dist-info\\RECORD',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\RECORD',
'DATA'),
('cryptography-45.0.2.dist-info\\METADATA',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\METADATA',
'DATA'),
('cryptography-45.0.2.dist-info\\WHEEL',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\WHEEL',
'DATA'),
('cryptography-45.0.2.dist-info\\INSTALLER',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\INSTALLER',
'DATA'),
('cryptography-45.0.2.dist-info\\licenses\\LICENSE.APACHE',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\licenses\\LICENSE.APACHE',
'DATA'),
('base_library.zip',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\base_library.zip',
'DATA')],
[],
False,
False,
1779102721,
[('run.exe',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\run.exe',
'EXECUTABLE')],
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll')

View File

@@ -1,189 +0,0 @@
('D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\pc_agent.pkg',
{'BINARY': True,
'DATA': True,
'EXECUTABLE': True,
'EXTENSION': True,
'PYMODULE': True,
'PYSOURCE': True,
'PYZ': False,
'SPLASH': True,
'SYMLINK': False},
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz', 'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\PYZ-00.pyz', 'PYZ'),
('struct',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth_inspect',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
'PYSOURCE'),
('pyi_rth_cryptography_openssl',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_cryptography_openssl.py',
'PYSOURCE'),
('pyi_rth_pywintypes',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_pywintypes.py',
'PYSOURCE'),
('pyi_rth_pythoncom',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_pythoncom.py',
'PYSOURCE'),
('pc_agent', 'D:\\이태훈\\22전산자산조사\\ITAM\\pc_agent.py', 'PYSOURCE'),
('python312.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll',
'BINARY'),
('pywin32_system32\\pywintypes312.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\pywin32_system32\\pywintypes312.dll',
'BINARY'),
('pywin32_system32\\pythoncom312.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\pywin32_system32\\pythoncom312.dll',
'BINARY'),
('select.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\select.pyd',
'EXTENSION'),
('_multiprocessing.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_multiprocessing.pyd',
'EXTENSION'),
('pyexpat.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\pyexpat.pyd',
'EXTENSION'),
('_ssl.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_ssl.pyd',
'EXTENSION'),
('_hashlib.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_hashlib.pyd',
'EXTENSION'),
('unicodedata.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\unicodedata.pyd',
'EXTENSION'),
('_decimal.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_decimal.pyd',
'EXTENSION'),
('_lzma.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_lzma.pyd',
'EXTENSION'),
('_bz2.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_bz2.pyd',
'EXTENSION'),
('_ctypes.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_ctypes.pyd',
'EXTENSION'),
('_queue.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_queue.pyd',
'EXTENSION'),
('_wmi.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_wmi.pyd',
'EXTENSION'),
('_socket.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_socket.pyd',
'EXTENSION'),
('_overlapped.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_overlapped.pyd',
'EXTENSION'),
('_asyncio.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_asyncio.pyd',
'EXTENSION'),
('_cffi_backend.cp312-win_amd64.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\_cffi_backend.cp312-win_amd64.pyd',
'EXTENSION'),
('cryptography\\hazmat\\bindings\\_rust.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography\\hazmat\\bindings\\_rust.pyd',
'EXTENSION'),
('charset_normalizer\\md__mypyc.cp312-win_amd64.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\charset_normalizer\\md__mypyc.cp312-win_amd64.pyd',
'EXTENSION'),
('charset_normalizer\\md.cp312-win_amd64.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\charset_normalizer\\md.cp312-win_amd64.pyd',
'EXTENSION'),
('win32\\_win32sysloader.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\_win32sysloader.pyd',
'EXTENSION'),
('win32\\win32api.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\win32api.pyd',
'EXTENSION'),
('Pythonwin\\win32ui.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\Pythonwin\\win32ui.pyd',
'EXTENSION'),
('win32\\win32event.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\win32event.pyd',
'EXTENSION'),
('win32\\win32trace.pyd',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\win32\\win32trace.pyd',
'EXTENSION'),
('VCRUNTIME140.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\VCRUNTIME140.dll',
'BINARY'),
('VCRUNTIME140_1.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\VCRUNTIME140_1.dll',
'BINARY'),
('libssl-3.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libssl-3.dll',
'BINARY'),
('libcrypto-3.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libcrypto-3.dll',
'BINARY'),
('libffi-8.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libffi-8.dll',
'BINARY'),
('python3.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\python3.dll',
'BINARY'),
('Pythonwin\\mfc140u.dll',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\Pythonwin\\mfc140u.dll',
'BINARY'),
('certifi\\cacert.pem',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\certifi\\cacert.pem',
'DATA'),
('certifi\\py.typed',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\certifi\\py.typed',
'DATA'),
('cryptography-45.0.2.dist-info\\licenses\\LICENSE.BSD',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\licenses\\LICENSE.BSD',
'DATA'),
('cryptography-45.0.2.dist-info\\licenses\\LICENSE',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\licenses\\LICENSE',
'DATA'),
('cryptography-45.0.2.dist-info\\RECORD',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\RECORD',
'DATA'),
('cryptography-45.0.2.dist-info\\METADATA',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\METADATA',
'DATA'),
('cryptography-45.0.2.dist-info\\WHEEL',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\WHEEL',
'DATA'),
('cryptography-45.0.2.dist-info\\INSTALLER',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\INSTALLER',
'DATA'),
('cryptography-45.0.2.dist-info\\licenses\\LICENSE.APACHE',
'C:\\Users\\User\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\cryptography-45.0.2.dist-info\\licenses\\LICENSE.APACHE',
'DATA'),
('base_library.zip',
'D:\\이태훈\\22전산자산조사\\ITAM\\build\\pc_agent\\base_library.zip',
'DATA')],
'python312.dll',
False,
False,
False,
[],
None,
None,
None)

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,58 +0,0 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean these modules are required for running your program. Both
Python's standard library and 3rd-party Python packages often conditionally
import optional modules, some of which may be available only on certain
platforms.
Types of import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), netrc (delayed, conditional), getpass (delayed)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional)
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
missing module named fcntl - imported by subprocess (optional)
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
missing module named _scproxy - imported by urllib.request (conditional)
missing module named termios - imported by getpass (optional)
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named pyimod02_importers - imported by C:\Users\User\AppData\Local\Programs\Python\Python312\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
missing module named collections.Callable - imported by collections (optional), socks (optional)
missing module named vms_lib - imported by platform (delayed, optional)
missing module named 'java.lang' - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named _winreg - imported by platform (delayed, optional)
missing module named simplejson - imported by requests.compat (conditional, optional)
missing module named dummy_threading - imported by requests.cookies (optional)
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
missing module named annotationlib - imported by typing_extensions (conditional)
missing module named 'h2.events' - imported by urllib3.http2.connection (top-level)
missing module named 'h2.connection' - imported by urllib3.http2.connection (top-level)
missing module named h2 - imported by urllib3.http2.connection (top-level)
missing module named zstandard - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named brotli - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named brotlicffi - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named win_inet_pton - imported by socks (conditional, optional)
missing module named bcrypt - imported by cryptography.hazmat.primitives.serialization.ssh (optional)
missing module named cryptography.x509.UnsupportedExtension - imported by cryptography.x509 (optional), urllib3.contrib.pyopenssl (optional)
missing module named 'OpenSSL.crypto' - imported by urllib3.contrib.pyopenssl (delayed, conditional)
missing module named OpenSSL - imported by urllib3.contrib.pyopenssl (top-level)
missing module named 'pyodide.ffi' - imported by urllib3.contrib.emscripten.fetch (delayed, optional)
missing module named pyodide - imported by urllib3.contrib.emscripten.fetch (top-level)
missing module named js - imported by urllib3.contrib.emscripten.fetch (top-level)
missing module named 'win32com.gen_py' - imported by win32com (conditional, optional)

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function initDB() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306'),
multipleStatements: true
});
console.log('🔄 DB 초기화 시작 (영문 표준 스키마 적용)...');
const tablesToDrop = [
'pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets',
'sw_sub_assets', 'sw_perm_assets', 'cloud_assets', 'sw_users', 'asset_logs'
];
for (const table of tablesToDrop) {
await connection.query(`DROP TABLE IF EXISTS ${table}`);
}
const createHardwareTable = (tableName, comment) => `
CREATE TABLE ${tableName} (
id VARCHAR(50) PRIMARY KEY,
corp VARCHAR(100),
asset_code VARCHAR(100),
purchase_date VARCHAR(50),
type VARCHAR(50),
detail_purpose VARCHAR(50),
purpose VARCHAR(255),
details TEXT,
current_org VARCHAR(255),
prev_org VARCHAR(255),
location VARCHAR(255),
manager_main VARCHAR(100),
manager_sub VARCHAR(100),
ip_address VARCHAR(100),
remote_tool VARCHAR(100),
server_id VARCHAR(100),
server_pw VARCHAR(100),
model_name VARCHAR(255),
mainboard VARCHAR(255) COMMENT '메인보드',
os VARCHAR(100),
cpu VARCHAR(255),
ram VARCHAR(100),
gpu VARCHAR(100),
storage1 VARCHAR(255),
storage2 VARCHAR(255),
storage3 VARCHAR(255),
monitoring VARCHAR(100),
price VARCHAR(100),
remarks TEXT,
storage_location VARCHAR(255),
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
await connection.query(createHardwareTable('pc_assets', 'PC'));
await connection.query(createHardwareTable('server_assets', 'Server'));
await connection.query(createHardwareTable('storage_assets', 'Storage'));
await connection.query(createHardwareTable('equip_assets', 'Equipment'));
await connection.query(createHardwareTable('mobile_assets', 'Mobile'));
await connection.query(`
CREATE TABLE sw_sub_assets (
id VARCHAR(50) PRIMARY KEY,
corp VARCHAR(100) COMMENT '구매법인',
category VARCHAR(100) COMMENT '분야',
dept VARCHAR(100) COMMENT '부서',
product_name VARCHAR(255) COMMENT '제품명',
license_type VARCHAR(100) COMMENT '라이선스 유형',
quantity INT COMMENT '수량',
price VARCHAR(100) COMMENT '금액',
purchase_date VARCHAR(50) COMMENT '구매일',
start_date VARCHAR(50) COMMENT '시작일',
expiry_date VARCHAR(50) COMMENT '만료일',
vendor VARCHAR(255) COMMENT '구매업체',
remarks TEXT COMMENT '비고',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE sw_perm_assets (
id VARCHAR(50) PRIMARY KEY,
corp VARCHAR(100) COMMENT '구매법인',
category VARCHAR(100) COMMENT '분야',
dept VARCHAR(100) COMMENT '부서',
product_name VARCHAR(255) COMMENT '제품명',
license_key VARCHAR(255) COMMENT '라이선스 키',
quantity INT COMMENT '수량',
price VARCHAR(100) COMMENT '금액',
purchase_date VARCHAR(50) COMMENT '구매일',
start_date VARCHAR(50) COMMENT '시작일',
expiry_date VARCHAR(50) COMMENT '만료일',
vendor VARCHAR(255) COMMENT '구매업체',
remarks TEXT COMMENT '비고',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE cloud_assets (
id VARCHAR(50) PRIMARY KEY,
platform_name VARCHAR(100),
corp VARCHAR(100),
dept VARCHAR(100),
product_name VARCHAR(255),
account_name VARCHAR(255),
pay_method VARCHAR(100),
pay_day VARCHAR(50),
card_num VARCHAR(100),
monthly_fee VARCHAR(100),
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE sw_users (
id INT AUTO_INCREMENT PRIMARY KEY,
sw_id VARCHAR(50),
corp VARCHAR(100),
dept VARCHAR(100),
position VARCHAR(50),
user_name VARCHAR(100),
usage_period VARCHAR(100),
doc_name VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE asset_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50),
log_date VARCHAR(50),
log_user VARCHAR(100),
details TEXT,
cost DECIMAL(15,2) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE ops_domain_assets (
id VARCHAR(50) PRIMARY KEY,
type VARCHAR(50) COMMENT '유형',
corp VARCHAR(100) COMMENT '법인',
service_name VARCHAR(255) COMMENT '서비스명',
domain_name VARCHAR(255) COMMENT '관리도메인',
start_date VARCHAR(50) COMMENT '시작일',
expiry_date VARCHAR(50) COMMENT '만료일',
price VARCHAR(100) COMMENT '금액',
manager_main VARCHAR(100) COMMENT '담당자',
manager_sub VARCHAR(100) COMMENT '담당자(부)',
remarks TEXT COMMENT '비고',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
console.log('✅ 모든 테이블이 영문 표준 스키마로 재생성되었습니다.');
await connection.end();
}
initDB().catch(err => {
console.error('❌ DB 초기화 실패:', err);
process.exit(1);
});

73
docker-compose.prod.yaml Normal file
View File

@@ -0,0 +1,73 @@
services:
backend:
image: itam-backend:prod
build:
context: .
dockerfile: Dockerfile.backend.prod
container_name: itam-backend
working_dir: /app
env_file:
- .env
environment:
NODE_ENV: production
PORT: 3000
volumes:
- ./uploads:/app/uploads
- ./map_config.json:/app/map_config.json:ro
expose:
- "3000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
frontend:
image: itam-frontend:prod
build:
context: .
dockerfile: Dockerfile.frontend.prod
container_name: itam-frontend
expose:
- "80"
restart: unless-stopped
nginx:
image: nginx:stable-alpine
container_name: itam-nginx
ports:
- "9090:80"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./logs/nginx:/var/log/nginx
depends_on:
backend:
condition: service_healthy
frontend:
condition: service_started
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
database:
image: mysql:latest
container_name: itam-mysql
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=itam1234 # 여기 직접 기입
- MYSQL_DATABASE=itam
- MYSQL_USER=itam
- MYSQL_PASSWORD=itam1234
volumes:
- ./mysql_data:/var/lib/mysql
restart: always
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci

62
docker-compose.test.yaml Normal file
View File

@@ -0,0 +1,62 @@
# Local testing compose file - uses relative paths and build contexts
# Usage: docker compose -f docker-compose.test.yaml up --build
services:
backend:
build:
context: .
dockerfile: Dockerfile.backend.prod
container_name: itam-backend-test
working_dir: /app
env_file:
- .env
environment:
NODE_ENV: development
PORT: 3000
DB_HOST: ${DB_HOST:-172.16.8.151}
DB_PORT: ${DB_PORT:-3306}
DB_USER: ${DB_USER:-root}
DB_PASS: ${DB_PASS:-}
DB_NAME: ${DB_NAME:-itam}
ports:
- "3000:3000"
volumes:
- ./uploads:/app/uploads
- ./map_config.json:/app/map_config.json:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
frontend:
build:
context: .
dockerfile: Dockerfile.frontend.prod
container_name: itam-frontend-test
expose:
- "80"
restart: unless-stopped
nginx:
image: nginx:stable-alpine
container_name: itam-nginx-test
ports:
- "8080:80"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./logs/nginx:/var/log/nginx
depends_on:
backend:
condition: service_healthy
frontend:
condition: service_started
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s

48
docker-compose.yaml Normal file
View File

@@ -0,0 +1,48 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile.backend
container_name: dachs-backend
working_dir: /app
env_file:
- .env
environment:
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_USER: ${DB_USER}
DB_PASS: ${DB_PASS}
DB_NAME: ${DB_NAME}
PORT: 3000
ports:
- "3000:3000"
volumes:
- ./:/app
- backend_node_modules:/app/node_modules
- ./uploads:/app/uploads
- ./map_config.json:/app/map_config.json
command: npm run server
restart: unless-stopped
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
container_name: dachs-frontend
working_dir: /app
depends_on:
- backend
environment:
CHOKIDAR_USEPOLLING: "true"
VITE_DEV_PROXY_TARGET: http://backend:3000
ports:
- "8080:8080"
volumes:
- ./:/app
- frontend_node_modules:/app/node_modules
command: npm run dev -- --host 0.0.0.0
restart: unless-stopped
volumes:
backend_node_modules:
frontend_node_modules:

View File

@@ -0,0 +1,55 @@
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
# Logging
access_log /var/log/nginx/frontend-access.log main;
error_log /var/log/nginx/frontend-error.log warn;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip compression
gzip on;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/json application/javascript;
gzip_min_length 1000;
# Serve static files with SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets (60 days)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 60d;
add_header Cache-Control "public, immutable";
}
# Don't cache HTML files
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Health check
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# Deny access to sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

View File

@@ -0,0 +1,16 @@
# MySQL init directory
This directory is kept as a legacy hook for file-based MySQL initialization.
Current production path in this repository is not file-based import.
The live Docker flow uses the `db-bootstrap` service in `docker-compose.yaml` to stream data from the external source DB into the internal `db` container.
Use this directory only if you intentionally switch back to `docker-entrypoint-initdb.d` style initialization.
If you do that, typical naming would be:
- `01_schema.sql`
- `02_seed.sql`
- or a single `01_itam_dump.sql`
Remember that files in this directory are executed automatically by the MySQL container only on the first initialization of the data volume.

101
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,101 @@
upstream backend {
server backend:3000;
}
upstream frontend {
server frontend:80;
}
server {
listen 80;
listen [::]:80;
server_name _;
# Client upload size limit (adjust as needed)
client_max_body_size 100M;
# Logging
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip compression
gzip on;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/json application/javascript;
gzip_min_length 1000;
# Forward all app requests to the frontend container
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
}
# API proxy to backend
location /api/ {
proxy_pass http://backend/api/;
# Preserve original request information
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Port $server_port;
# Connection settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# Uploads proxy to backend
location /uploads/ {
proxy_pass http://backend/uploads/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache uploads
expires 30d;
add_header Cache-Control "public";
}
# Health check endpoint (for monitoring)
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# Deny access to sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ ~$ {
deny all;
access_log off;
log_not_found off;
}
}

View File

@@ -1,45 +1,45 @@
# [Issue] 소프트웨어 자산 관리 체계 개편 및 클라우드(Cloud) 서비스 관리 신설 # [Issue] 소프트웨어 자산 관리 체계 개편 및 클라우드(Cloud) 서비스 관리 신설
## 1. 개요 ## 1. 개요
기존의 단일 소프트웨어(SW) 분류 체계를 비즈니스 모델에 맞춰 **구독형, 영구형, 클라우드형**으로 삼원화하고, 특히 비용 변동이 잦은 클라우드 서비스를 독립적으로 관리할 수 있는 전용 시스템을 신설함. 기존의 단일 소프트웨어(SW) 분류 체계를 비즈니스 모델에 맞춰 **구독형, 영구형, 클라우드형**으로 삼원화하고, 특히 비용 변동이 잦은 클라우드 서비스를 독립적으로 관리할 수 있는 전용 시스템을 신설함.
--- ---
## 2. 주요 작업 내용 ## 2. 주요 작업 내용
### 📂 소프트웨어 관리 프레임워크 재구조화 ### 📂 소프트웨어 관리 프레임워크 재구조화
- **분류 체계 개편**: 소프트웨어를 아래 세 가지 유형으로 재정의하여 관리 효율성을 높임. - **분류 체계 개편**: 소프트웨어를 아래 세 가지 유형으로 재정의하여 관리 효율성을 높임.
1. **구독형 (Subscription)**: 연/월 정액제로 운영되는 SW 1. **구독형 (Subscription)**: 연/월 정액제로 운영되는 SW
2. **영구형 (Perpetual)**: 구매 후 영구 소유하는 SW (유지보수 중심 관리) 2. **영구형 (Perpetual)**: 구매 후 영구 소유하는 SW (유지보수 중심 관리)
3. **클라우드형 (Cloud)**: 플랫폼 기반 종량제(AWS, Azure 등) 서비스 3. **클라우드형 (Cloud)**: 플랫폼 기반 종량제(AWS, Azure 등) 서비스
- **내비게이션 통합**: 상단 탭을 유형별로 분리하여 각 자산 특성에 맞는 리스트 뷰를 제공함. - **내비게이션 통합**: 상단 탭을 유형별로 분리하여 각 자산 특성에 맞는 리스트 뷰를 제공함.
### ☁️ 클라우드(Cloud) 서비스 관리 페이지 신설 ### ☁️ 클라우드(Cloud) 서비스 관리 페이지 신설
- **전용 리스트 뷰 (`CloudListView.ts`)**: - **전용 리스트 뷰 (`CloudListView.ts`)**:
- 플랫폼명, 담당 부서, 프로젝트(사용용도), 결제 수단, 결제일 등 클라우드 특화 항목 중심의 테이블 구성함. - 플랫폼명, 담당 부서, 프로젝트(사용용도), 결제 수단, 결제일 등 클라우드 특화 항목 중심의 테이블 구성함.
- **결제수단별 필터링 기능** (법인카드, 인보이스) 및 통합 검색 기능을 추가함. - **결제수단별 필터링 기능** (법인카드, 인보이스) 및 통합 검색 기능을 추가함.
- **클라우드 전문 모달 (`CloudModal.ts`)**: - **클라우드 전문 모달 (`CloudModal.ts`)**:
- 클라우드 요금 및 결제 정보 입력을 위한 2분할 레이아웃 배치함. - 클라우드 요금 및 결제 정보 입력을 위한 2분할 레이아웃 배치함.
- **업데이트 이력(History Logs)** 시스템을 도입하여 매월 변동되는 비용을 히스토리 형식으로 기록/추적 가능하게 함. - **업데이트 이력(History Logs)** 시스템을 도입하여 매월 변동되는 비용을 히스토리 형식으로 기록/추적 가능하게 함.
### 📊 대시보드(Dashboard) 리팩토링 및 고도화 ### 📊 대시보드(Dashboard) 리팩토링 및 고도화
- **카드 레이아웃 최적화**: 사용율, 만료 예정, 클라우드 현황(전월/당월 비교) 정보를 2열 그리드로 정돈함. - **카드 레이아웃 최적화**: 사용율, 만료 예정, 클라우드 현황(전월/당월 비교) 정보를 2열 그리드로 정돈함.
- **데이터 시각화**: - **데이터 시각화**:
- **클라우드 결제 규모 추이**: 최근 4개월간의 비용 변동을 꺾은선 그래프로 구현함. - **클라우드 결제 규모 추이**: 최근 4개월간의 비용 변동을 꺾은선 그래프로 구현함.
- **실시간 데이터 연동**: 자산 업데이트 이력(Logs)에 기록된 비용이 대시보드 차트에 실시간 합산 반영되도록 로그 분석 엔진을 구축함. - **실시간 데이터 연동**: 자산 업데이트 이력(Logs)에 기록된 비용이 대시보드 차트에 실시간 합산 반영되도록 로그 분석 엔진을 구축함.
- **상세 팝업 연동**: 대시보드 요약 카드를 클릭하면 해당하는 자산의 상세 목록이 뜨는 모달 연동 기능을 추가함. - **상세 팝업 연동**: 대시보드 요약 카드를 클릭하면 해당하는 자산의 상세 목록이 뜨는 모달 연동 기능을 추가함.
### 🪟 UX 및 데이터 정합성 강화 ### 🪟 UX 및 데이터 정합성 강화
- **수정 저장 워크플로우 (Edit-to-Save)**: 실수로 인한 데이터 변경을 막기 위해 모든 상세 모달에 '조회 모드'를 기본으로 하고, [수정] 버튼 클릭 시에만 입력이 활성화되도록 제어함. - **수정 저장 워크플로우 (Edit-to-Save)**: 실수로 인한 데이터 변경을 막기 위해 모든 상세 모달에 '조회 모드'를 기본으로 하고, [수정] 버튼 클릭 시에만 입력이 활성화되도록 제어함.
- **금액 자동 포맷팅**: 콤마 표시 오류를 해결하고 천 단위 포맷팅을 표준화함. - **금액 자동 포맷팅**: 콤마 표시 오류를 해결하고 천 단위 포맷팅을 표준화함.
- **결제 임박 알림**: 각 서비스의 결제일을 계산하여 14일 이내 결제가 필요한 항목을 대시보드에서 즉시 파악할 수 있게 함. - **결제 임박 알림**: 각 서비스의 결제일을 계산하여 14일 이내 결제가 필요한 항목을 대시보드에서 즉시 파악할 수 있게 함.
--- ---
## 3. 향후 과제 ## 3. 향후 과제
- 클라우드 플랫폼 간 비용 비교 통계 기능 확장 검토 - 클라우드 플랫폼 간 비용 비교 통계 기능 확장 검토
- 결제 수단(법인카드) 만료일에 기초한 알림 서비스 추가 검토 - 결제 수단(법인카드) 만료일에 기초한 알림 서비스 추가 검토
--- ---
**작업자**: Antigravity (AI Assistant) **작업자**: Antigravity (AI Assistant)
**상태**: 완료 (2026-04-17) **상태**: 완료 (2026-04-17)

View File

@@ -1,33 +1,33 @@
# [이슈] S/W 자산 관리 고도화 및 이력 추적 기능 구현 # [이슈] S/W 자산 관리 고도화 및 이력 추적 기능 구현
## 1. 개요 ## 1. 개요
소프트웨어 자산의 라이프사이클을 체계적으로 관리하기 위해 상세 정보 모달을 개편하고, 갱신(업데이트) 이력을 추적할 수 있는 기능을 구현하였습니다. 또한, 사용자의 가독성을 위해 상태를 나타내는 자동 뱃지를 도입하고 날짜 입력 편의성을 개선하였습니다. 소프트웨어 자산의 라이프사이클을 체계적으로 관리하기 위해 상세 정보 모달을 개편하고, 갱신(업데이트) 이력을 추적할 수 있는 기능을 구현하였습니다. 또한, 사용자의 가독성을 위해 상태를 나타내는 자동 뱃지를 도입하고 날짜 입력 편의성을 개선하였습니다.
## 2. 작업 상세 내용 ## 2. 작업 상세 내용
### A. S/W 목록(Table) 개선 ### A. S/W 목록(Table) 개선
- **상태 자동 계산 시스템 도입**: - **상태 자동 계산 시스템 도입**:
- 구독 S/W: 만료일 기준 **[사용중] / [만료]** 자동 표시. - 구독 S/W: 만료일 기준 **[사용중] / [만료]** 자동 표시.
- 영구 S/W: 유지보수 대상 여부에 따라 **[유효] / [없음]** 표시. - 영구 S/W: 유지보수 대상 여부에 따라 **[유효] / [없음]** 표시.
- **UI 뱃지 적용**: 테이블 좌측에 상태 뱃지를 추가하여 시각적 인지도를 높임. - **UI 뱃지 적용**: 테이블 좌측에 상태 뱃지를 추가하여 시각적 인지도를 높임.
### B. 상세 정보 모달 개편 (`SWModal.ts`) ### B. 상세 정보 모달 개편 (`SWModal.ts`)
- **2단 분할 레이아웃 적용**: 좌측(기본 정보), 우측(업데이트 타임라인)으로 UI 재설계. - **2단 분할 레이아웃 적용**: 좌측(기본 정보), 우측(업데이트 타임라인)으로 UI 재설계.
- **날짜 입력 필드 개선**: - **날짜 입력 필드 개선**:
- '구매일' 필드에 캘린더 피커(Calendar Picker) 적용. - '구매일' 필드에 캘린더 피커(Calendar Picker) 적용.
- '구독 기간' 필드를 **시작일**과 **종료일**로 분리하여 각각 캘린더 적용. - '구독 기간' 필드를 **시작일**과 **종료일**로 분리하여 각각 캘린더 적용.
- 직접 입력("yyyy-mm-dd") 형식도 동시 지원. - 직접 입력("yyyy-mm-dd") 형식도 동시 지원.
### C. 계약 업데이트(갱신) 관리 기능 ### C. 계약 업데이트(갱신) 관리 기능
- **[업데이트 추가]** 버튼 및 전용 서브 팝업 구현. - **[업데이트 추가]** 버튼 및 전용 서브 팝업 구현.
- 갱신 시 발생하는 비용, 기간 연장, 메모를 기록하여 타임라인(Log)에 누적. - 갱신 시 발생하는 비용, 기간 연장, 메모를 기록하여 타임라인(Log)에 누적.
- 업데이트 반영 시 메인 자산 정보의 구독 기한 및 누적 금액이 자동으로 최신화되도록 연동. - 업데이트 반영 시 메인 자산 정보의 구독 기한 및 누적 금액이 자동으로 최신화되도록 연동.
## 3. 관련 파일 ## 3. 관련 파일
- `src/views/SW_Table.ts`: 테이블 상태 로직 및 뱃지 렌더링. - `src/views/SW_Table.ts`: 테이블 상태 로직 및 뱃지 렌더링.
- `src/components/Modal/SWModal.ts`: 모달 UI 및 날짜 처리, 업데이트 로직. - `src/components/Modal/SWModal.ts`: 모달 UI 및 날짜 처리, 업데이트 로직.
- `src/styles/modal.css`: 분할 레이아웃 및 타임라인 스타일. - `src/styles/modal.css`: 분할 레이아웃 및 타임라인 스타일.
## 4. 확인 사항 ## 4. 확인 사항
- 엑셀 업로드/다운로드 시 기존 '구독일' 문자열 형식과의 호환성 유지 확인. - 엑셀 업로드/다운로드 시 기존 '구독일' 문자열 형식과의 호환성 유지 확인.
- 브라우저 테스트를 통한 캘린더 작동 및 테이블 상태 연동 확인 완료. - 브라우저 테스트를 통한 캘린더 작동 및 테이블 상태 연동 확인 완료.

226
docs/itam_cicd_setup.md Normal file
View File

@@ -0,0 +1,226 @@
# ITAM CI/CD 설정 가이드
## 1. 문서 목적
이 문서는 현재 ITAM 저장소에 구성된 CI/CD 파일을 실제 운영에 연결하기 위해 필요한 기준을 정리한 가이드다.
대상 범위는 아래와 같다.
1. Gitea Actions workflow 역할
2. Gitea Variables / Secrets 설정값
3. 운영 서버 배포 디렉토리 기준
4. 운영 영속 경로 기준
5. production deploy 실행 전 확인 사항
---
## 2. 현재 CI/CD 구성
현재 `.gitea/workflows`에는 ITAM 관련 workflow만 남겨둔 상태다.
1. `itam_code_check.yml`
2. `itam_docker_build_check.yml`
3. `itam_production_deploy.yml`
각 workflow의 역할은 아래와 같다.
1. `itam_code_check.yml`: TypeScript/Vite build와 compose 문법 검증
2. `itam_docker_build_check.yml`: 운영용 Docker 이미지 빌드 가능 여부 검증
3. `itam_production_deploy.yml`: 운영 서버에 SSH 접속 후 실제 배포 수행
현재 배포 흐름은 아래와 같다.
```mermaid
flowchart LR
DEV["Developer Push or Manual Run"] --> CODE["ITAM Code Check"]
CODE --> BUILD["ITAM Docker Build Check"]
BUILD --> DEPLOY["ITAM Production Deploy"]
DEPLOY --> HOST["Production Host"]
HOST --> APP["docker compose up -d --build"]
linkStyle default stroke:#d32f2f,stroke-width:2px;
```
---
## 3. Gitea Variables / Secrets 기준
`itam_production_deploy.yml`이 정상 동작하려면 아래 값이 필요하다.
### 3.1 Variables
아래 항목은 Gitea repository Variables에 등록한다.
| Key | 설명 | 예시 |
| --- | --- | --- |
| `PROD_HOST` | 운영 서버 SSH 접속 호스트 | `10.0.0.25` |
| `PROD_USER` | 운영 서버 SSH 사용자 | `deploy` |
| `PROD_DEPLOY_PATH` | 서버에서 저장소를 배포할 경로 | `/opt/itam` |
| `PROD_BACKUP_ROOT` | 배포 전 백업 저장 경로, 배포 경로 바깥이어야 함 | `/opt/itam-backups` |
| `PROD_GIT_URL` | 운영 서버에서 pull 가능한 저장소 주소 | `git@gitea.example.com:team/itam.git` |
| `PROD_DB_HOST` | 외부 MySQL 호스트 | `172.16.8.151` |
| `PROD_DB_PORT` | 외부 MySQL 포트 | `3306` |
| `PROD_DB_USER` | 운영 DB 계정 | `itam_admin` |
| `PROD_DB_NAME` | 운영 DB 이름 | `itam` |
| `PROD_LOG_LEVEL` | 애플리케이션 로그 레벨 | `info` |
### 3.2 Secrets
아래 항목은 Gitea repository Secrets에 등록한다.
| Key | 설명 |
| --- | --- |
| `PROD_SSH_PRIVATE_KEY` | 운영 서버 접속용 개인키 |
| `PROD_DB_PASS` | 운영 DB 비밀번호 |
### 3.3 운영 원칙
1. `PROD_DB_PASS`는 Variables가 아니라 Secrets에만 둔다.
2. `PROD_SSH_PRIVATE_KEY`는 배포 전용 계정 키를 사용한다.
3. `PROD_GIT_URL`은 운영 서버에서 직접 pull 가능한 주소여야 한다.
4. 운영 서버의 `known_hosts`는 workflow에서 자동 등록되지만, 최초 운영 전 수동 접속 검증도 함께 수행하는 것이 안전하다.
5. `PROD_BACKUP_ROOT``PROD_DEPLOY_PATH` 내부가 아니라 바깥 경로를 사용해야 한다.
---
## 4. 운영 서버 배포 디렉토리 기준
현재 `itam_production_deploy.yml`은 운영 서버에서 아래 흐름으로 배포를 수행한다.
1. `PROD_DEPLOY_PATH` 디렉토리를 생성한다.
2. 기존 운영 상태가 있으면 배포 전 백업을 수행한다.
3. 해당 경로에 저장소를 clone 또는 fetch 한다.
4. 지정 브랜치로 checkout 한다.
5. `uploads`, `logs/nginx` 디렉토리를 생성한다.
6. `.env.deploy`를 서버의 `.env`로 복사한다.
7. `docker compose -f docker-compose.prod.yaml up -d --build`를 실행한다.
권장 디렉토리 구조는 아래와 같다.
```text
/opt/itam/
.env
docker-compose.prod.yaml
Dockerfile.frontend.prod
Dockerfile.backend.prod
map_config.json
uploads/
logs/
nginx/
docker/
nginx/
default.conf
frontend/
default.conf
src/
public/
img/
/opt/itam-backups/
db/
files/
```
현재 구조 기준 배포 관계는 아래와 같다.
```mermaid
flowchart TB
subgraph HOST["Production Host"]
REPO["PROD_DEPLOY_PATH"]
ENV[".env"]
UP["uploads/"]
LOGS["logs/nginx/"]
MAP["map_config.json"]
end
subgraph CTR["Docker Services"]
NGINX["itam-nginx"]
FRONT["itam-frontend"]
BACK["itam-backend"]
end
DB["External MySQL"]
REPO --> NGINX
ENV --> BACK
UP --> BACK
MAP --> BACK
LOGS --> NGINX
REPO --> BAK["PROD_BACKUP_ROOT"]
NGINX --> FRONT
NGINX --> BACK
BACK --> DB
linkStyle default stroke:#d32f2f,stroke-width:2px;
```
---
## 5. 영속 경로 기준
현재 `docker-compose.prod.yaml` 기준으로 운영에서 유지되어야 하는 경로는 아래와 같다.
1. `.env`
2. `uploads/`
3. `map_config.json`
4. `logs/nginx/`
5. `PROD_BACKUP_ROOT`
각 경로의 의미는 아래와 같다.
1. `.env`: backend 런타임 환경변수
2. `uploads/`: 업로드 파일 데이터
3. `map_config.json`: 위치/맵 구성 데이터
4. `logs/nginx/`: reverse proxy 접근 로그 및 에러 로그
5. `PROD_BACKUP_ROOT`: 배포 전 DB dump와 운영 파일 아카이브 저장 위치
운영 기준으로 보면 `uploads/``map_config.json`은 애플리케이션 데이터이고, `.env`는 환경 설정이며, `logs/nginx/`는 운영 추적 데이터다.
즉 서버 운영 시 컨테이너만 다시 띄우면 되는 구조가 아니라, 이 경로들이 유지되는 것을 전제로 배포가 성립한다.
---
## 6. 배포 전 체크리스트
`itam_production_deploy.yml` 실행 전 아래 항목을 먼저 확인한다.
1. 운영 서버에 Docker Engine과 `docker compose`가 설치되어 있어야 한다.
2. 운영 서버의 배포 계정이 Docker 실행 권한을 가져야 한다.
3. 운영 서버에서 `PROD_GIT_URL`로 직접 `git fetch`가 가능해야 한다.
4. 외부 MySQL 접속 정보가 실제 운영망 기준으로 열려 있어야 한다.
5. 운영 서버에 `map_config.json` 초기 파일이 존재해야 한다.
6. 방화벽 또는 보안 장비에서 80 포트 접근 정책이 정리되어 있어야 한다.
권장 확인 명령 예시는 아래와 같다.
```bash
docker --version
docker compose version
git ls-remote <PROD_GIT_URL>
test -f map_config.json
test -d uploads
```
---
## 7. 현재 구조에서의 해석
현재 ITAM CI/CD는 staging 없이도 운영 배포가 가능한 최소 구조로 정리되어 있다.
이 구조의 장점은 아래와 같다.
1. workflow 수가 적어서 관리가 단순하다.
2. 운영 배포에 필요한 변수와 시크릿 범위가 명확하다.
3. staging이 필요해지면 production deploy workflow를 복제해 별도 환경으로 확장하기 쉽다.
즉, 지금 단계에서는 production 기준을 먼저 고정하고, staging은 동일 패턴으로 추후 추가하는 전략이 적절하다.
---
## 8. 다음 권장 작업
현재 문서 기준으로 바로 이어서 할 작업은 아래 순서가 적절하다.
1. `PROD_DEPLOY_PATH` 실제 서버 경로 확정
2. 운영 서버 배포 계정 생성 및 SSH 키 등록
3. `map_config.json`, `uploads/` 초기 데이터 준비
4. production deploy workflow에 smoke check 추가
5. 로그 로테이션과 백업/복구 절차 문서화

View File

@@ -1,64 +1,64 @@
# [보고서] IT 자산 실시간 통합 관리 시스템(RMM) 도입 계획서 # [보고서] IT 자산 실시간 통합 관리 시스템(RMM) 도입 계획서
## 1. 도입 배경 및 목적 ## 1. 도입 배경 및 목적
- **현황**: 현재 시스템은 수동 입력 기반의 정적 자산 대장으로 운영되어, 실제 장비의 가동 상태나 장애 여부를 실시간으로 파악하는 데 한계가 있음. - **현황**: 현재 시스템은 수동 입력 기반의 정적 자산 대장으로 운영되어, 실제 장비의 가동 상태나 장애 여부를 실시간으로 파악하는 데 한계가 있음.
- **목적**: 전산자산(서버, PC)의 실시간 상태 정보를 자동 수집하고 장애 징후를 사전에 탐지하여, 선제적 유지보수 체계를 구축하고 운영 효율성을 극대화함. - **목적**: 전산자산(서버, PC)의 실시간 상태 정보를 자동 수집하고 장애 징후를 사전에 탐지하여, 선제적 유지보수 체계를 구축하고 운영 효율성을 극대화함.
## 2. 시스템 주요 기능 ## 2. 시스템 주요 기능
### 2.1 실시간 가동 상태 모니터링 ### 2.1 실시간 가동 상태 모니터링
- 주요 자원(CPU, Memory, Disk) 사용률 실시간 수집 - 주요 자원(CPU, Memory, Disk) 사용률 실시간 수집
- 운영체제(OS) 및 주요 시스템 서비스의 정상 작동 여부 확인 - 운영체제(OS) 및 주요 시스템 서비스의 정상 작동 여부 확인
- 자산 리스트 내 상태 인디케이터(정상/주의/장애) 표시 - 자산 리스트 내 상태 인디케이터(정상/주의/장애) 표시
### 2.2 원격 제어 및 지원 통합 ### 2.2 원격 제어 및 지원 통합
- **기술적 구현 방식 (원클릭 자동 연결)**: 웹사이트에서 전화번호를 누르면 전화 앱이 켜지거나, 이메일 주소를 누르면 메일 창이 뜨는 것과 동일한 원리인 'URL 프로토콜 핸들러' 기술을 적용함. - **기술적 구현 방식 (원클릭 자동 연결)**: 웹사이트에서 전화번호를 누르면 전화 앱이 켜지거나, 이메일 주소를 누르면 메일 창이 뜨는 것과 동일한 원리인 'URL 프로토콜 핸들러' 기술을 적용함.
- **자동화 프로세스**: 관리자가 화면의 [연결] 버튼을 클릭하면, 시스템이 팀뷰어나 애니데스크에 "A장비로 연결해줘"라는 신호를 직접 보냄. - **자동화 프로세스**: 관리자가 화면의 [연결] 버튼을 클릭하면, 시스템이 팀뷰어나 애니데스크에 "A장비로 연결해줘"라는 신호를 직접 보냄.
- **편의성**: 관리자가 대상 장비의 ID나 비밀번호를 직접 복사해서 프로그램에 입력할 필요 없이, 클릭 한 번으로 내 PC에 설치된 원격 소프트웨어가 자동 실행되며 즉시 화면이 연결되도록 구현함. - **편의성**: 관리자가 대상 장비의 ID나 비밀번호를 직접 복사해서 프로그램에 입력할 필요 없이, 클릭 한 번으로 내 PC에 설치된 원격 소프트웨어가 자동 실행되며 즉시 화면이 연결되도록 구현함.
- **유연한 접속 모드 지원**: - **유연한 접속 모드 지원**:
- **무인 접속(Unattended Access)**: 서버 및 공용 장비의 경우, 사전에 등록된 자격 증명을 통해 관리자 승인만으로 즉시 접속하여 야간 또는 긴급 장애에 대응함. - **무인 접속(Unattended Access)**: 서버 및 공용 장비의 경우, 사전에 등록된 자격 증명을 통해 관리자 승인만으로 즉시 접속하여 야간 또는 긴급 장애에 대응함.
- **사용자 승인 접속(Attended Access)**: 개인용 PC의 경우, 사용자의 화면에 접속 요청 팝업을 띄우고 승인 시에만 화면 공유를 시작하여 개인정보 보호 및 보안 규정을 준수함. - **사용자 승인 접속(Attended Access)**: 개인용 PC의 경우, 사용자의 화면에 접속 요청 팝업을 띄우고 승인 시에만 화면 공유를 시작하여 개인정보 보호 및 보안 규정을 준수함.
- **보안 및 감사 로그 자동화**: - **보안 및 감사 로그 자동화**:
- 원격 접속이 시작되는 시점에 관리자 정보, 접속 목적, 대상 장비 정보를 DB에 자동 기록함. - 원격 접속이 시작되는 시점에 관리자 정보, 접속 목적, 대상 장비 정보를 DB에 자동 기록함.
- 세션 종료 후 총 작업 시간 및 조치 내역을 입력하도록 유도하여 투명한 유지보수 이력을 관리함. - 세션 종료 후 총 작업 시간 및 조치 내역을 입력하도록 유도하여 투명한 유지보수 이력을 관리함.
### 2.3 원격 지원 상세 워크플로우 (Remote Support Workflow) ### 2.3 원격 지원 상세 워크플로우 (Remote Support Workflow)
관리자가 장애를 인지하고 조치를 완료하기까지의 표준 프로세스는 다음과 같습니다. 관리자가 장애를 인지하고 조치를 완료하기까지의 표준 프로세스는 다음과 같습니다.
1. **지원 요청 및 대상 선택**: 관리자가 ITAM 대시보드 또는 리스트에서 장애가 발생한 자산을 선택하고 '원격 지원 시작' 버튼을 클릭함. 1. **지원 요청 및 대상 선택**: 관리자가 ITAM 대시보드 또는 리스트에서 장애가 발생한 자산을 선택하고 '원격 지원 시작' 버튼을 클릭함.
2. **접속 모드 자동 판별**: 2. **접속 모드 자동 판별**:
- **서버(무인)**: 시스템이 저장된 자격 증명을 확인하고 관리자에게 '즉시 연결' 팝업을 띄움. - **서버(무인)**: 시스템이 저장된 자격 증명을 확인하고 관리자에게 '즉시 연결' 팝업을 띄움.
- **PC(유인)**: 관리자가 '접속 요청' 버튼을 누르면, 대상 PC 화면에 "관리자가 원격 제어를 요청했습니다. 승인하시겠습니까?" 팝업이 전송됨. - **PC(유인)**: 관리자가 '접속 요청' 버튼을 누르면, 대상 PC 화면에 "관리자가 원격 제어를 요청했습니다. 승인하시겠습니까?" 팝업이 전송됨.
3. **세션 초기화 및 로그 생성**: 접속 시도가 승인되면 서버는 즉시 [접속 일시, 관리자 ID, 대상 자산 번호]를 포함한 '세션 로그'를 생성하고 상태를 '진행 중'으로 변경함. 3. **세션 초기화 및 로그 생성**: 접속 시도가 승인되면 서버는 즉시 [접속 일시, 관리자 ID, 대상 자산 번호]를 포함한 '세션 로그'를 생성하고 상태를 '진행 중'으로 변경함.
4. **프로토콜 핸들러 실행**: 브라우저가 관리자 PC의 원격 제어 앱(TeamViewer 등)을 자동으로 실행하며, 대상 장비의 ID와 패스워드 정보를 암호화된 인자로 전달하여 즉시 화면이 연결됨. 4. **프로토콜 핸들러 실행**: 브라우저가 관리자 PC의 원격 제어 앱(TeamViewer 등)을 자동으로 실행하며, 대상 장비의 ID와 패스워드 정보를 암호화된 인자로 전달하여 즉시 화면이 연결됨.
5. **조치 및 지원 수행**: 관리자가 실시간으로 장비를 제어하여 장애를 복구함. 5. **조치 및 지원 수행**: 관리자가 실시간으로 장비를 제어하여 장애를 복구함.
6. **세션 종료 및 결과 기록**: 6. **세션 종료 및 결과 기록**:
- 관리자가 원격 제어 앱을 종료하면, ITAM 웹 화면에 '조치 결과 입력' 창이 활성화됨. - 관리자가 원격 제어 앱을 종료하면, ITAM 웹 화면에 '조치 결과 입력' 창이 활성화됨.
- 관리자가 조치 내용(예: 서비스 재시작, 패치 적용 등)을 입력하고 저장하면 세션 로그가 최종 확정됨. - 관리자가 조치 내용(예: 서비스 재시작, 패치 적용 등)을 입력하고 저장하면 세션 로그가 최종 확정됨.
7. **이력 보관**: 완료된 모든 이력은 '자산 상세 정보 > 유지보수 이력' 탭에서 언제든지 열람 및 보고서 출력이 가능함. 7. **이력 보관**: 완료된 모든 이력은 '자산 상세 정보 > 유지보수 이력' 탭에서 언제든지 열람 및 보고서 출력이 가능함.
### 3.3 장애 사전 탐지 및 알림 ### 3.3 장애 사전 탐지 및 알림
- 설정된 임계치(예: 디스크 잔량 10% 미만) 초과 시 즉시 알림 발송 - 설정된 임계치(예: 디스크 잔량 10% 미만) 초과 시 즉시 알림 발송
- 장기 미접속 또는 점검 누락 장비의 실시간 식별 - 장기 미접속 또는 점검 누락 장비의 실시간 식별
## 3. 운영 프로세스 및 메커니즘 ## 3. 운영 프로세스 및 메커니즘
1. **데이터 수집 (Collection)**: 각 자산에 배치된 에이전트가 시스템 정보를 주기적으로 추출함. 1. **데이터 수집 (Collection)**: 각 자산에 배치된 에이전트가 시스템 정보를 주기적으로 추출함.
2. **분석 및 판별 (Analysis)**: 수집된 데이터를 중앙 서버에서 분석하여 장비의 상태 등급을 판정함. 2. **분석 및 판별 (Analysis)**: 수집된 데이터를 중앙 서버에서 분석하여 장비의 상태 등급을 판정함.
3. **가시화 (Visualization)**: 통합 관리 대시보드를 통해 전체 자산의 헬스 상태를 실시간으로 출력함. 3. **가시화 (Visualization)**: 통합 관리 대시보드를 통해 전체 자산의 헬스 상태를 실시간으로 출력함.
4. **대응 (Action)**: 장애 감지 시 원격 제어 기능을 호출하여 즉각적인 기술 지원을 수행함. 4. **대응 (Action)**: 장애 감지 시 원격 제어 기능을 호출하여 즉각적인 기술 지원을 수행함.
## 4. 핵심 기술 및 도구 ## 4. 핵심 기술 및 도구
- **에이전트**: PowerShell 기반의 경량 스크립트를 활용하여 별도의 상용 소프트웨어 설치 없이 시스템 정보 수집. - **에이전트**: PowerShell 기반의 경량 스크립트를 활용하여 별도의 상용 소프트웨어 설치 없이 시스템 정보 수집.
- **백엔드**: Node.js 환경에서 대용량 점검 데이터를 효율적으로 처리하고 데이터베이스화함. - **백엔드**: Node.js 환경에서 대용량 점검 데이터를 효율적으로 처리하고 데이터베이스화함.
- **프론트엔드**: TypeScript를 활용하여 직관적이고 반응성이 뛰어난 관리자 대시보드 구현. - **프론트엔드**: TypeScript를 활용하여 직관적이고 반응성이 뛰어난 관리자 대시보드 구현.
- **원격 솔루션**: 보안성이 검증된 TeamViewer/AnyDesk의 프로토콜 연동을 통한 안전한 원격 접속 환경 구축. - **원격 솔루션**: 보안성이 검증된 TeamViewer/AnyDesk의 프로토콜 연동을 통한 안전한 원격 접속 환경 구축.
## 5. 기대 효과 ## 5. 기대 효과
- **가용성 증대**: 장애 발생 전 사전 조치를 통해 시스템 다운타임을 최소화하고 업무 연속성 확보. - **가용성 증대**: 장애 발생 전 사전 조치를 통해 시스템 다운타임을 최소화하고 업무 연속성 확보.
- **비용 절감**: 현장 방문 점검 최소화 및 원격 조치를 통한 IT 운영 관리 비용 및 시간 절감. - **비용 절감**: 현장 방문 점검 최소화 및 원격 조치를 통한 IT 운영 관리 비용 및 시간 절감.
- **데이터 기반 의무**: 객관적인 성능 지표 및 점검 이력을 바탕으로 정밀한 자산 교체 주기 산정 및 감사 대응. - **데이터 기반 의무**: 객관적인 성능 지표 및 점검 이력을 바탕으로 정밀한 자산 교체 주기 산정 및 감사 대응.
- **관리 생산성 향상**: 자산 정보 조회와 실시간 관리를 단일 플랫폼으로 통합하여 업무 프로세스 간소화. - **관리 생산성 향상**: 자산 정보 조회와 실시간 관리를 단일 플랫폼으로 통합하여 업무 프로세스 간소화.
## 6. 향후 계획 ## 6. 향후 계획
- 1단계: 서버 자산 중심의 실시간 모니터링 및 대시보드 구축 - 1단계: 서버 자산 중심의 실시간 모니터링 및 대시보드 구축
- 2단계: 전사 PC 대상 원격 지원 및 보안 점검 기능 확대 적용 - 2단계: 전사 PC 대상 원격 지원 및 보안 점검 기능 확대 적용
- 3단계: 누적 데이터를 활용한 성능 분석 및 월간 운영 보고서 자동화 - 3단계: 누적 데이터를 활용한 성능 분석 및 월간 운영 보고서 자동화

View File

@@ -1,318 +1,318 @@
# 전산자산 원격 점검 및 관리 시스템(RMM) 구축 조사 보고서 (상세판) # 전산자산 원격 점검 및 관리 시스템(RMM) 구축 조사 보고서 (상세판)
## 1. RMM(Remote Monitoring & Management) 개요 ## 1. RMM(Remote Monitoring & Management) 개요
RMM(Remote Monitoring & Management)은 서버, 업무용 PC, 노트북 등 IT 자산에 에이전트를 설치하여 RMM(Remote Monitoring & Management)은 서버, 업무용 PC, 노트북 등 IT 자산에 에이전트를 설치하여
중앙 관리 서버에서 상태를 자동 수집하고, 이상 발생 시 경고를 발송하며, 필요 시 원격 접속으로 문제를 해결하는 중앙 관리 서버에서 상태를 자동 수집하고, 이상 발생 시 경고를 발송하며, 필요 시 원격 접속으로 문제를 해결하는
기업용 IT 운영 관리 체계입니다. 기업용 IT 운영 관리 체계입니다.
### 주요 기능 ### 주요 기능
- CPU, 메모리, 디스크 상태 모니터링 - CPU, 메모리, 디스크 상태 모니터링
- Windows 서비스 및 프로세스 상태 점검 - Windows 서비스 및 프로세스 상태 점검
- OS 패치 및 백신 상태 확인 - OS 패치 및 백신 상태 확인
- 자동 점검 스케줄링 (1일 1~2회 이상) - 자동 점검 스케줄링 (1일 1~2회 이상)
- 이상 발생 시 이메일/메신저 알림 - 이상 발생 시 이메일/메신저 알림
- 원격 접속을 통한 장애 조치 - 원격 접속을 통한 장애 조치
- 점검 이력 및 감사 로그 보관 - 점검 이력 및 감사 로그 보관
--- ---
## 2. 구축 목표 ## 2. 구축 목표
### 서버 및 서버용 PC ### 서버 및 서버용 PC
- 하루 1~2회 자동 점검 - 하루 1~2회 자동 점검
- 주요 시스템 자원 및 서비스 상태 수집 - 주요 시스템 자원 및 서비스 상태 수집
- 이상 발생 시 관리자 즉시 통보 - 이상 발생 시 관리자 즉시 통보
### 업무용 PC ### 업무용 PC
- 중앙 관리 서버에서 정기 점검 - 중앙 관리 서버에서 정기 점검
- 패치 및 보안 상태 확인 - 패치 및 보안 상태 확인
### 개인 PC ### 개인 PC
- 사용자가 직접 점검 실행 - 사용자가 직접 점검 실행
- 결과를 중앙 서버에 업로드 - 결과를 중앙 서버에 업로드
### 관리자 ### 관리자
- 마지막 점검 일시 확인 - 마지막 점검 일시 확인
- 성공/실패 여부 확인 - 성공/실패 여부 확인
- 미실행 장비 식별 - 미실행 장비 식별
- 필요 시 즉시 원격 접속 - 필요 시 즉시 원격 접속
--- ---
## 3. 기대 효과 ## 3. 기대 효과
- 장애 조기 탐지 및 사전 예방 - 장애 조기 탐지 및 사전 예방
- 현장 방문 최소화 - 현장 방문 최소화
- 점검 누락 방지 - 점검 누락 방지
- 감사 대응 자료 자동 확보 - 감사 대응 자료 자동 확보
- 자산 운영 현황 실시간 가시화 - 자산 운영 현황 실시간 가시화
- 사용자 점검 이행 여부 관리 - 사용자 점검 이행 여부 관리
--- ---
## 4. 전체 시스템 아키텍처 ## 4. 전체 시스템 아키텍처
```text ```text
[관리자 웹 포털] [관리자 웹 포털]
├─ 대시보드 ├─ 대시보드
├─ 점검 결과 조회 ├─ 점검 결과 조회
├─ 원격 접속 버튼 ├─ 원격 접속 버튼
├─ 알림 관리 ├─ 알림 관리
└─ 사용자 수행 현황 └─ 사용자 수행 현황
[중앙 관리 서버] [중앙 관리 서버]
├─ 스케줄러 ├─ 스케줄러
├─ 데이터 수집 API ├─ 데이터 수집 API
├─ 분석 엔진 ├─ 분석 엔진
├─ 알림 시스템 ├─ 알림 시스템
└─ 데이터베이스 └─ 데이터베이스
[에이전트 설치 대상] [에이전트 설치 대상]
├─ 서버 ├─ 서버
├─ 서버용 PC ├─ 서버용 PC
├─ 업무용 PC ├─ 업무용 PC
└─ 개인 PC └─ 개인 PC
``` ```
--- ---
## 5. 주요 구성 요소 ## 5. 주요 구성 요소
### 5.1 중앙 관리 서버 ### 5.1 중앙 관리 서버
- 스케줄 실행 - 스케줄 실행
- 상태 분석 - 상태 분석
- 데이터 저장 - 데이터 저장
- 알림 전송 - 알림 전송
- 웹 서비스 제공 - 웹 서비스 제공
### 5.2 에이전트 프로그램 ### 5.2 에이전트 프로그램
- PowerShell 또는 Python 기반 - PowerShell 또는 Python 기반
- 상태 수집 - 상태 수집
- 중앙 서버 전송 - 중앙 서버 전송
### 5.3 관리자 웹 대시보드 ### 5.3 관리자 웹 대시보드
- 실시간 현황 조회 - 실시간 현황 조회
- 점검 이력 확인 - 점검 이력 확인
- 원격 접속 실행 - 원격 접속 실행
### 5.4 원격 접속 솔루션 ### 5.4 원격 접속 솔루션
- TeamViewer Tensor - TeamViewer Tensor
- AnyDesk - AnyDesk
- Microsoft Remote Help - Microsoft Remote Help
### 5.5 데이터베이스 ### 5.5 데이터베이스
- SQL Server 또는 PostgreSQL - SQL Server 또는 PostgreSQL
### 5.6 알림 시스템 ### 5.6 알림 시스템
- 이메일 - 이메일
- Microsoft Teams - Microsoft Teams
- Slack - Slack
--- ---
## 6. 점검 항목 ## 6. 점검 항목
### 공통 점검 항목 ### 공통 점검 항목
- CPU 사용률 - CPU 사용률
- 메모리 사용률 - 메모리 사용률
- 디스크 여유 공간 - 디스크 여유 공간
- 네트워크 연결 상태 - 네트워크 연결 상태
- 시스템 부팅 시간 - 시스템 부팅 시간
- 재부팅 필요 여부 - 재부팅 필요 여부
### 서버 추가 항목 ### 서버 추가 항목
- 주요 서비스 실행 여부 - 주요 서비스 실행 여부
- 이벤트 로그 오류 - 이벤트 로그 오류
- 백업 결과 - 백업 결과
- DB 상태 - DB 상태
### PC 추가 항목 ### PC 추가 항목
- 백신 업데이트 여부 - 백신 업데이트 여부
- Windows Update 상태 - Windows Update 상태
- BitLocker 상태 - BitLocker 상태
### 개인 PC ### 개인 PC
- 기본 시스템 상태 - 기본 시스템 상태
- 점검 수행 여부 및 시간 기록 - 점검 수행 여부 및 시간 기록
--- ---
## 7. 운영 프로세스 ## 7. 운영 프로세스
### 정상 운영 ### 정상 운영
1. 스케줄러가 하루 1~2회 자동 실행 1. 스케줄러가 하루 1~2회 자동 실행
2. 에이전트가 점검 수행 2. 에이전트가 점검 수행
3. 결과를 중앙 서버로 전송 3. 결과를 중앙 서버로 전송
4. 분석 엔진이 정상 여부 판정 4. 분석 엔진이 정상 여부 판정
5. 대시보드에 저장 5. 대시보드에 저장
### 이상 발생 시 ### 이상 발생 시
1. 임계치 초과 또는 서비스 중지 감지 1. 임계치 초과 또는 서비스 중지 감지
2. 관리자에게 알림 발송 2. 관리자에게 알림 발송
3. 관리자가 원격 접속 3. 관리자가 원격 접속
4. 조치 내용 기록 4. 조치 내용 기록
### 개인 PC ### 개인 PC
1. 사용자가 '점검 실행' 버튼 클릭 1. 사용자가 '점검 실행' 버튼 클릭
2. 스크립트 수행 2. 스크립트 수행
3. 결과 업로드 3. 결과 업로드
4. 관리자가 이행 여부 확인 4. 관리자가 이행 여부 확인
--- ---
## 8. 개인 PC 자가 점검 기능 ## 8. 개인 PC 자가 점검 기능
### 사용자 화면 ### 사용자 화면
- 점검 실행 버튼 - 점검 실행 버튼
- 결과 요약 표시 - 결과 요약 표시
- 마지막 점검 시간 표시 - 마지막 점검 시간 표시
### 관리자 확인 항목 ### 관리자 확인 항목
- 마지막 점검 일시 - 마지막 점검 일시
- 성공/실패 여부 - 성공/실패 여부
- 미실행 기간 - 미실행 기간
- 이상 발생 내역 - 이상 발생 내역
--- ---
## 9. 관리자 대시보드 구성 ## 9. 관리자 대시보드 구성
- 전체 자산 현황 - 전체 자산 현황
- 정상/경고/장애 통계 - 정상/경고/장애 통계
- 최근 점검 성공률 - 최근 점검 성공률
- 미점검 장비 목록 - 미점검 장비 목록
- 개인 PC 수행 현황 - 개인 PC 수행 현황
- 원격 접속 바로가기 - 원격 접속 바로가기
- 월간 보고서 - 월간 보고서
--- ---
## 10. 솔루션 비교 ## 10. 솔루션 비교
| 솔루션 | 특징 | 적합도 | | 솔루션 | 특징 | 적합도 |
|------|------|------| |------|------|------|
| Microsoft Intune | 엔드포인트 관리 및 규정 준수 | 매우 높음 | | Microsoft Intune | 엔드포인트 관리 및 규정 준수 | 매우 높음 |
| TeamViewer Tensor | 기업용 원격 접속 및 RMM 연동 | 매우 높음 | | TeamViewer Tensor | 기업용 원격 접속 및 RMM 연동 | 매우 높음 |
| ManageEngine Endpoint Central | 자산, 패치, 원격 관리 통합 | 매우 높음 | | ManageEngine Endpoint Central | 자산, 패치, 원격 관리 통합 | 매우 높음 |
| Zabbix | 오픈소스 모니터링 | 높음 | | Zabbix | 오픈소스 모니터링 | 높음 |
| Splashtop Remote Support | 원격 지원 + RMM | 높음 | | Splashtop Remote Support | 원격 지원 + RMM | 높음 |
| Power BI | 대시보드 및 보고 | 매우 높음 | | Power BI | 대시보드 및 보고 | 매우 높음 |
--- ---
## 11. 권장 구축 방안 ## 11. 권장 구축 방안
### 권장 아키텍처 ### 권장 아키텍처
- Microsoft Intune - Microsoft Intune
- TeamViewer Tensor - TeamViewer Tensor
- PowerShell 자동 점검 스크립트 - PowerShell 자동 점검 스크립트
- Microsoft SQL Server - Microsoft SQL Server
- Power BI - Power BI
- Microsoft Teams 알림 - Microsoft Teams 알림
### 권장 이유 ### 권장 이유
- Windows 환경과 높은 호환성 - Windows 환경과 높은 호환성
- 보안 및 감사 기능 우수 - 보안 및 감사 기능 우수
- 사용자 PC까지 통합 관리 가능 - 사용자 PC까지 통합 관리 가능
- 경영진 보고 자동화 가능 - 경영진 보고 자동화 가능
--- ---
## 12. 보안 요구사항 ## 12. 보안 요구사항
- MFA(다중 인증) - MFA(다중 인증)
- RBAC(역할 기반 권한 관리) - RBAC(역할 기반 권한 관리)
- TLS 암호화 - TLS 암호화
- 감사 로그 저장 - 감사 로그 저장
- 승인된 관리자만 원격 접속 - 승인된 관리자만 원격 접속
- 사용자 동의 기반 개인 PC 점검 - 사용자 동의 기반 개인 PC 점검
--- ---
## 13. 구축 일정 (예시) ## 13. 구축 일정 (예시)
| 단계 | 기간 | | 단계 | 기간 |
|------|------| |------|------|
| 요구사항 분석 | 2주 | | 요구사항 분석 | 2주 |
| 솔루션 선정 | 2주 | | 솔루션 선정 | 2주 |
| PoC | 4주 | | PoC | 4주 |
| 설계 및 개발 | 6주 | | 설계 및 개발 | 6주 |
| 시범 운영 | 4주 | | 시범 운영 | 4주 |
| 전사 확대 | 4주 | | 전사 확대 | 4주 |
총 예상 기간: 약 4~6개월 총 예상 기간: 약 4~6개월
--- ---
## 14. 예상 비용 (예시) ## 14. 예상 비용 (예시)
| 항목 | 비용 수준 | | 항목 | 비용 수준 |
|------|----------| |------|----------|
| Intune 라이선스 | 사용자당 월 과금 | | Intune 라이선스 | 사용자당 월 과금 |
| TeamViewer Tensor | 동시 세션 기준 | | TeamViewer Tensor | 동시 세션 기준 |
| 개발 비용 | 중~고 | | 개발 비용 | 중~고 |
| 운영 비용 | 중간 | | 운영 비용 | 중간 |
--- ---
## 15. 구축 우선순위 ## 15. 구축 우선순위
### 1단계 ### 1단계
- 핵심 서버 모니터링 - 핵심 서버 모니터링
- 관리자 대시보드 - 관리자 대시보드
### 2단계 ### 2단계
- 원격 접속 통합 - 원격 접속 통합
- 자동 알림 - 자동 알림
### 3단계 ### 3단계
- 개인 PC 자가 점검 - 개인 PC 자가 점검
### 4단계 ### 4단계
- Power BI 경영 보고 - Power BI 경영 보고
--- ---
## 16. 최종 권장안 ## 16. 최종 권장안
> Microsoft Intune + TeamViewer Tensor + PowerShell + SQL Server + Power BI > Microsoft Intune + TeamViewer Tensor + PowerShell + SQL Server + Power BI
이 조합은 다음 요구사항을 모두 충족합니다. 이 조합은 다음 요구사항을 모두 충족합니다.
- 자동 점검 - 자동 점검
- 이상 탐지 - 이상 탐지
- 원격 접속 - 원격 접속
- 사용자 자가 점검 - 사용자 자가 점검
- 이력 관리 - 이력 관리
- 감사 대응 - 감사 대응
- 경영진 보고 - 경영진 보고
--- ---
## 17. 공식 출처 및 링크 ## 17. 공식 출처 및 링크
- Microsoft Intune: https://intune.microsoft.com - Microsoft Intune: https://intune.microsoft.com
- TeamViewer Tensor: https://www.teamviewer.com/en/tensor/ - TeamViewer Tensor: https://www.teamviewer.com/en/tensor/
- TeamViewer RMM 소개: https://www.teamviewer.com/en/solutions/use-cases/rmm-remote-monitoring-management/ - TeamViewer RMM 소개: https://www.teamviewer.com/en/solutions/use-cases/rmm-remote-monitoring-management/
- ManageEngine Endpoint Central: https://www.manageengine.com/products/endpoint-central/ - ManageEngine Endpoint Central: https://www.manageengine.com/products/endpoint-central/
- Zabbix: https://www.zabbix.com - Zabbix: https://www.zabbix.com
- Power BI: https://powerbi.microsoft.com - Power BI: https://powerbi.microsoft.com
- Microsoft SQL Server: https://www.microsoft.com/sql-server - Microsoft SQL Server: https://www.microsoft.com/sql-server
- Splashtop RMM 설명: https://www.splashtop.com/blog/what-is-remote-monitoring-and-management - Splashtop RMM 설명: https://www.splashtop.com/blog/what-is-remote-monitoring-and-management
--- ---
## 18. 결론 ## 18. 결론
본 시스템은 서버, 업무용 PC, 개인 PC를 통합 관리하여 본 시스템은 서버, 업무용 PC, 개인 PC를 통합 관리하여
정기적인 자동 점검과 이상 탐지, 원격 접속, 사용자 자가 점검, 점검 이력 관리까지 지원하는 정기적인 자동 점검과 이상 탐지, 원격 접속, 사용자 자가 점검, 점검 이력 관리까지 지원하는
기업용 IT 운영 플랫폼입니다. 기업용 IT 운영 플랫폼입니다.
특히 개인 PC의 자가 점검 기능과 관리자 추적 기능을 포함함으로써 특히 개인 PC의 자가 점검 기능과 관리자 추적 기능을 포함함으로써
규정 준수와 운영 효율성을 동시에 확보할 수 있습니다. 규정 준수와 운영 효율성을 동시에 확보할 수 있습니다.

View File

@@ -1,379 +1,379 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=device-width, initial-scale=1.0">
<title>PC 사양 대시보드 시각화 개선 기획서</title> <title>PC 사양 대시보드 시각화 개선 기획서</title>
<!-- Google Fonts: Pretendard 대체용 Outfit & Noto Sans KR --> <!-- Google Fonts: Pretendard 대체용 Outfit & Noto Sans KR -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style> <style>
:root { :root {
--primary: #4F46E5; --primary: #4F46E5;
--primary-light: #EEF2FF; --primary-light: #EEF2FF;
--secondary: #10B981; --secondary: #10B981;
--secondary-light: #D1FAE5; --secondary-light: #D1FAE5;
--text-dark: #0F172A; --text-dark: #0F172A;
--text-muted: #64748B; --text-muted: #64748B;
--border-color: #E2E8F0; --border-color: #E2E8F0;
--bg-light: #F8FAFC; --bg-light: #F8FAFC;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body { body {
font-family: 'Outfit', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-family: 'Outfit', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: var(--text-dark); color: var(--text-dark);
background-color: #FFFFFF; background-color: #FFFFFF;
line-height: 1.6; line-height: 1.6;
letter-spacing: -0.02em; letter-spacing: -0.02em;
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
} }
.container { .container {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
} }
/* Header Styling */ /* Header Styling */
header { header {
border-bottom: 2px solid var(--text-dark); border-bottom: 2px solid var(--text-dark);
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.doc-category { .doc-category {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
color: var(--primary); color: var(--primary);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
h1 { h1 {
font-size: 2.25rem; font-size: 2.25rem;
font-weight: 900; font-weight: 900;
color: var(--text-dark); color: var(--text-dark);
line-height: 1.2; line-height: 1.2;
} }
.meta-info { .meta-info {
display: flex; display: flex;
gap: 1.5rem; gap: 1.5rem;
margin-top: 1rem; margin-top: 1rem;
font-size: 0.85rem; font-size: 0.85rem;
color: var(--text-muted); color: var(--text-muted);
} }
.meta-info span strong { .meta-info span strong {
color: var(--text-dark); color: var(--text-dark);
} }
/* Section Styling */ /* Section Styling */
section { section {
margin-bottom: 3rem; margin-bottom: 3rem;
} }
h2 { h2 {
font-size: 1.4rem; font-size: 1.4rem;
font-weight: 800; font-weight: 800;
color: var(--text-dark); color: var(--text-dark);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
h2::before { h2::before {
content: ''; content: '';
display: inline-block; display: inline-block;
width: 4px; width: 4px;
height: 18px; height: 18px;
background-color: var(--primary); background-color: var(--primary);
border-radius: 2px; border-radius: 2px;
} }
p { p {
font-size: 1rem; font-size: 1rem;
color: #334155; color: #334155;
margin-bottom: 1rem; margin-bottom: 1rem;
text-align: justify; text-align: justify;
} }
/* List & Card Styling */ /* List & Card Styling */
ul { ul {
list-style-position: inside; list-style-position: inside;
margin-left: 0.5rem; margin-left: 0.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
li { li {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: #334155; color: #334155;
} }
.spec-card { .spec-card {
background-color: var(--bg-light); background-color: var(--bg-light);
border-left: 4px solid var(--primary); border-left: 4px solid var(--primary);
border-radius: 4px; border-radius: 4px;
padding: 1.25rem; padding: 1.25rem;
margin: 1.5rem 0; margin: 1.5rem 0;
} }
.spec-card h3 { .spec-card h3 {
font-size: 1.05rem; font-size: 1.05rem;
font-weight: 700; font-weight: 700;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: var(--text-dark); color: var(--text-dark);
} }
/* Table Styling */ /* Table Styling */
.table-container { .table-container {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
margin: 1.5rem 0; margin: 1.5rem 0;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 8px;
} }
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 0.9rem; font-size: 0.9rem;
text-align: left; text-align: left;
} }
th { th {
background-color: var(--bg-light); background-color: var(--bg-light);
font-weight: 700; font-weight: 700;
color: var(--text-dark); color: var(--text-dark);
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
td { td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
color: #334155; color: #334155;
} }
tr:last-child td { tr:last-child td {
border-bottom: none; border-bottom: none;
} }
.badge { .badge {
display: inline-block; display: inline-block;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 4px; border-radius: 4px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
text-align: center; text-align: center;
} }
.badge-primary { .badge-primary {
color: var(--primary); color: var(--primary);
background-color: var(--primary-light); background-color: var(--primary-light);
} }
.badge-secondary { .badge-secondary {
color: var(--secondary); color: var(--secondary);
background-color: var(--secondary-light); background-color: var(--secondary-light);
} }
/* Highlight box */ /* Highlight box */
.note-box { .note-box {
background-color: #FFFBEB; background-color: #FFFBEB;
border: 1px solid #FCD34D; border: 1px solid #FCD34D;
border-radius: 6px; border-radius: 6px;
padding: 1rem; padding: 1rem;
margin: 1.5rem 0; margin: 1.5rem 0;
font-size: 0.95rem; font-size: 0.95rem;
color: #92400E; color: #92400E;
} }
.note-box strong { .note-box strong {
color: #78350F; color: #78350F;
} }
footer { footer {
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
padding-top: 1.5rem; padding-top: 1.5rem;
margin-top: 4rem; margin-top: 4rem;
text-align: center; text-align: center;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-muted); color: var(--text-muted);
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header> <header>
<div class="doc-category">기획 명세서 / Product Specification</div> <div class="doc-category">기획 명세서 / Product Specification</div>
<h1>PC 사양 대시보드 시각화 개선 기획서</h1> <h1>PC 사양 대시보드 시각화 개선 기획서</h1>
<div class="meta-info"> <div class="meta-info">
<span>기획부서: <strong>IT자산관리 태스크포스(TF)</strong></span> <span>기획부서: <strong>IT자산관리 태스크포스(TF)</strong></span>
<span>최종 수정일: <strong>2026. 05. 28</strong></span> <span>최종 수정일: <strong>2026. 05. 28</strong></span>
<span>문서 버전: <strong>v1.1 (실제 엑셀 데이터 반영)</strong></span> <span>문서 버전: <strong>v1.1 (실제 엑셀 데이터 반영)</strong></span>
</div> </div>
</header> </header>
<!-- 1. 개요 및 목적 --> <!-- 1. 개요 및 목적 -->
<section> <section>
<h2>기획 개요 및 목적</h2> <h2>기획 개요 및 목적</h2>
<p>본 기획은 법인별/직무별 PC 자산 사양 현황의 시각적 피로도를 낮추고 데이터 전달력을 고도화하기 위한 개선 작업을 목적으로 합니다. 기존 대시보드 레이아웃의 비정형 비율을 재정립하고, 평균 점수와 권장 점수의 비교 방식을 '다중 막대' 형태에서 <strong>'혼합형(막대 + 꺾은선) 차트'</strong>로 변경하여 대조 직관성을 극대화합니다.</p> <p>본 기획은 법인별/직무별 PC 자산 사양 현황의 시각적 피로도를 낮추고 데이터 전달력을 고도화하기 위한 개선 작업을 목적으로 합니다. 기존 대시보드 레이아웃의 비정형 비율을 재정립하고, 평균 점수와 권장 점수의 비교 방식을 '다중 막대' 형태에서 <strong>'혼합형(막대 + 꺾은선) 차트'</strong>로 변경하여 대조 직관성을 극대화합니다.</p>
</section> </section>
<!-- 2. 주요 개선 사항 --> <!-- 2. 주요 개선 사항 -->
<section> <section>
<h2>주요 개선 내역</h2> <h2>주요 개선 내역</h2>
<div class="spec-card"> <div class="spec-card">
<h3>① 가족사별 PC 사양 현황 레이아웃 고도화</h3> <h3>① 가족사별 PC 사양 현황 레이아웃 고도화</h3>
<ul> <ul>
<li><strong>가로 비율 정밀 제어 (1:2)</strong>: 평균 점수 리스트와 막대그래프의 가로 폭 비율을 <code>1 : 2</code>로 엄격하게 고정하여 반응형 레이아웃 환경에서도 깨짐 없는 균형미를 제공합니다.</li> <li><strong>가로 비율 정밀 제어 (1:2)</strong>: 평균 점수 리스트와 막대그래프의 가로 폭 비율을 <code>1 : 2</code>로 엄격하게 고정하여 반응형 레이아웃 환경에서도 깨짐 없는 균형미를 제공합니다.</li>
<li><strong>가독성 개선</strong>: 가족사 텍스트 크기를 <code>0.95rem</code>, 평균 사양 점수 텍스트 크기를 <code>1.05rem</code>으로 키우고 세로 행간 여백을 확보해 가시성을 향상시켰습니다.</li> <li><strong>가독성 개선</strong>: 가족사 텍스트 크기를 <code>0.95rem</code>, 평균 사양 점수 텍스트 크기를 <code>1.05rem</code>으로 키우고 세로 행간 여백을 확보해 가시성을 향상시켰습니다.</li>
</ul> </ul>
</div> </div>
<div class="spec-card"> <div class="spec-card">
<h3>② 직무별 PC 사양 평균 및 권장 점수 혼합 시각화</h3> <h3>② 직무별 PC 사양 평균 및 권장 점수 혼합 시각화</h3>
<ul> <ul>
<li><strong>혼합형 차트(Mixed Chart) 구성</strong>: 직무별 PC 사양 평균 점수는 <span class="badge badge-primary">막대(Bar)</span> 그래프로, 권장 PC 사양 점수는 그 위를 관통하는 <span class="badge badge-secondary">선(Line)</span> 그래프로 표현합니다.</li> <li><strong>혼합형 차트(Mixed Chart) 구성</strong>: 직무별 PC 사양 평균 점수는 <span class="badge badge-primary">막대(Bar)</span> 그래프로, 권장 PC 사양 점수는 그 위를 관통하는 <span class="badge badge-secondary">선(Line)</span> 그래프로 표현합니다.</li>
<li><strong>레이어 정렬 우선순위 적용</strong>: 차트 정의 시 권장 점수선(Line)이 평균 점수막대(Bar) 뒤에 가리지 않고 항상 맨 앞에 위치하도록 렌더링 우선순위(<code>order</code> 속성)를 명확히 지정합니다.</li> <li><strong>레이어 정렬 우선순위 적용</strong>: 차트 정의 시 권장 점수선(Line)이 평균 점수막대(Bar) 뒤에 가리지 않고 항상 맨 앞에 위치하도록 렌더링 우선순위(<code>order</code> 속성)를 명확히 지정합니다.</li>
<li><strong>정렬 원복</strong>: 수동 정렬을 지양하고, 직무별 실제 평균 PC 사양 점수가 높은 순으로 자동 내림차순 정렬되도록 하여 가장 자연스러운 시각화를 구축합니다.</li> <li><strong>정렬 원복</strong>: 수동 정렬을 지양하고, 직무별 실제 평균 PC 사양 점수가 높은 순으로 자동 내림차순 정렬되도록 하여 가장 자연스러운 시각화를 구축합니다.</li>
</ul> </ul>
</div> </div>
</section> </section>
<!-- 3. 데이터 정의 --> <!-- 3. 데이터 정의 -->
<section> <section>
<h2>직무별 평균 및 권장 사양 점수 스펙</h2> <h2>직무별 평균 및 권장 사양 점수 스펙</h2>
<p>실제 PC 자산 데이터(CPU 및 RAM 점수 연산 결과)와 관리자의 권장 기준선이 아래 명시된 대소 조건 관계를 완벽히 만족하도록 더미 데이터 및 초기 권장 스펙 기준을 재정의했습니다.</p> <p>실제 PC 자산 데이터(CPU 및 RAM 점수 연산 결과)와 관리자의 권장 기준선이 아래 명시된 대소 조건 관계를 완벽히 만족하도록 더미 데이터 및 초기 권장 스펙 기준을 재정의했습니다.</p>
<div class="note-box"> <div class="note-box">
<strong>대소 관계 정렬 순서 (실제 평균 점수 기준):</strong><br> <strong>대소 관계 정렬 순서 (실제 평균 점수 기준):</strong><br>
AI 개발자 ➔ 편집 디자이너 ➔ 3D 디자이너 ➔ UXUI 디자이너 ➔ 3D 개발자 ➔ 프로그램 개발자 ➔ BIM모델러 ➔ 엔지니어 ➔ 웹 개발자 ➔ 기획자 순서로 실제 평균 점수 순위가 자동 정렬되어 시각화됩니다. (감리원은 실제 자산 데이터 부재로 비교군에서 제외) AI 개발자 ➔ 편집 디자이너 ➔ 3D 디자이너 ➔ UXUI 디자이너 ➔ 3D 개발자 ➔ 프로그램 개발자 ➔ BIM모델러 ➔ 엔지니어 ➔ 웹 개발자 ➔ 기획자 순서로 실제 평균 점수 순위가 자동 정렬되어 시각화됩니다. (감리원은 실제 자산 데이터 부재로 비교군에서 제외)
</div> </div>
<div class="table-container"> <div class="table-container">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>정렬 순위</th> <th>정렬 순위</th>
<th>직무명</th> <th>직무명</th>
<th>실제 평균 사양 점수 (Bar)</th> <th>실제 평균 사양 점수 (Bar)</th>
<th>기본 권장 사양 점수 (기준)</th> <th>기본 권장 사양 점수 (기준)</th>
<th>대소 관계 평가</th> <th>대소 관계 평가</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>1</td> <td>1</td>
<td><strong>AI 개발자</strong></td> <td><strong>AI 개발자</strong></td>
<td>88.0 점</td> <td>88.0 점</td>
<td>95 점</td> <td>95 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>2</td> <td>2</td>
<td><strong>편집 디자이너</strong></td> <td><strong>편집 디자이너</strong></td>
<td>80.2 점</td> <td>80.2 점</td>
<td>75 점</td> <td>75 점</td>
<td><span class="badge badge-secondary">권장 스펙 충족</span></td> <td><span class="badge badge-secondary">권장 스펙 충족</span></td>
</tr> </tr>
<tr> <tr>
<td>3</td> <td>3</td>
<td><strong>3D 디자이너</strong></td> <td><strong>3D 디자이너</strong></td>
<td>78.4 점</td> <td>78.4 점</td>
<td>90 점</td> <td>90 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>4</td> <td>4</td>
<td><strong>UXUI 디자이너</strong></td> <td><strong>UXUI 디자이너</strong></td>
<td>72.7 점</td> <td>72.7 점</td>
<td>70 점</td> <td>70 점</td>
<td><span class="badge badge-secondary">권장 스펙 충족</span></td> <td><span class="badge badge-secondary">권장 스펙 충족</span></td>
</tr> </tr>
<tr> <tr>
<td>5</td> <td>5</td>
<td><strong>3D 개발자</strong></td> <td><strong>3D 개발자</strong></td>
<td>67.8 점</td> <td>67.8 점</td>
<td>90 점</td> <td>90 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>6</td> <td>6</td>
<td><strong>프로그램 개발자</strong></td> <td><strong>프로그램 개발자</strong></td>
<td>67.3 점</td> <td>67.3 점</td>
<td>80 점</td> <td>80 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>7</td> <td>7</td>
<td><strong>BIM모델러</strong></td> <td><strong>BIM모델러</strong></td>
<td>62.1 점</td> <td>62.1 점</td>
<td>75 점</td> <td>75 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>8</td> <td>8</td>
<td><strong>엔지니어</strong></td> <td><strong>엔지니어</strong></td>
<td>42.9 점</td> <td>42.9 점</td>
<td>60 점</td> <td>60 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>9</td> <td>9</td>
<td><strong>웹 개발자</strong></td> <td><strong>웹 개발자</strong></td>
<td>39.2 점</td> <td>39.2 점</td>
<td>75 점</td> <td>75 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>10</td> <td>10</td>
<td><strong>기획자</strong></td> <td><strong>기획자</strong></td>
<td>38.6 점</td> <td>38.6 점</td>
<td>50 점</td> <td>50 점</td>
<td><span class="badge badge-secondary">미달 (교체 요망)</span></td> <td><span class="badge badge-secondary">미달 (교체 요망)</span></td>
</tr> </tr>
<tr> <tr>
<td>11</td> <td>11</td>
<td><strong>감리원</strong></td> <td><strong>감리원</strong></td>
<td>-</td> <td>-</td>
<td>40.0 점</td> <td>40.0 점</td>
<td><span class="badge badge-secondary">데이터 없음</span></td> <td><span class="badge badge-secondary">데이터 없음</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</section> </section>
<!-- 4. 기술 구현 세부사항 --> <!-- 4. 기술 구현 세부사항 -->
<section> <section>
<h2>기술 구현 세부 사양</h2> <h2>기술 구현 세부 사양</h2>
<div class="spec-card" style="border-left-color: var(--secondary);"> <div class="spec-card" style="border-left-color: var(--secondary);">
<h3>차트 렌더링 옵션 (Chart.js v4.x+)</h3> <h3>차트 렌더링 옵션 (Chart.js v4.x+)</h3>
<p>평균 PC 사양 점수를 보여주는 데이터셋과 권장 PC 사양 점수를 보여주는 데이터셋을 하나의 Canvas 엘리먼트에 그리되, 레이어 겹침과 시인성을 확보하기 위해 다음 세부 옵션을 바인딩합니다.</p> <p>평균 PC 사양 점수를 보여주는 데이터셋과 권장 PC 사양 점수를 보여주는 데이터셋을 하나의 Canvas 엘리먼트에 그리되, 레이어 겹침과 시인성을 확보하기 위해 다음 세부 옵션을 바인딩합니다.</p>
<ul> <ul>
<li><strong>Average Dataset</strong>: <code>type: 'bar', order: 2, backgroundColor: '#6366F1'</code></li> <li><strong>Average Dataset</strong>: <code>type: 'bar', order: 2, backgroundColor: '#6366F1'</code></li>
<li><strong>Recommended Dataset</strong>: <code>type: 'line', order: 1, borderColor: '#10B981', borderWidth: 3, pointRadius: 4, fill: false</code></li> <li><strong>Recommended Dataset</strong>: <code>type: 'line', order: 1, borderColor: '#10B981', borderWidth: 3, pointRadius: 4, fill: false</code></li>
<li><strong>정렬 로직</strong>: <code>Object.keys(jobScores).sort((a, b) => jobScores[b].avg - jobScores[a].avg)</code></li> <li><strong>정렬 로직</strong>: <code>Object.keys(jobScores).sort((a, b) => jobScores[b].avg - jobScores[a].avg)</code></li>
</ul> </ul>
</div> </div>
</section> </section>
<footer> <footer>
<p>&copy; 2026 HM ITAM Systems. All rights reserved.</p> <p>&copy; 2026 HM ITAM Systems. All rights reserved.</p>
</footer> </footer>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,60 @@
# 자산 이력 누적 관리 시스템 (Cumulative Asset History System) 구현 계획
본 문서는 자산의 라이프사이클(조직, 사용자, 용도, 상태 변동)을 체계적으로 추적하고 누적 관리하기 위한 기술적 설계 및 단계별 구현 계획을 담고 있습니다.
## 1. 목적
- 자산 정보 수정 시 중요 변경 사항을 자동으로 감지하여 이력(Log)화
- 과거부터 현재까지의 변동 사항을 타임라인 형태로 시각화하여 자산 흐름 파악
- 데이터 정합성을 위해 서버 측에서 변경 전/후 스냅샷 비교 방식 채택
## 2. 관리 대상 이력 (Watch Fields)
다음 항목의 변경이 발생할 경우 이력을 자동 생성합니다.
1. **조직 변동**: `current_dept` (현 사용조직) ↔ `previous_dept` 업데이트 포함
2. **사용자 변동**: `user_current` (현 사용자) ↔ `previous_user` 업데이트 포함
3. **용도 변경**: `asset_type`, `current_role` (예: 개인PC -> 공용PC)
4. **상태 변경**: `hw_status` (예: 운영 -> 수리, 재고 -> 폐기 등)
## 3. 기술 설계 (Technical Design)
### A. 데이터베이스 (DB)
- **대상 테이블**: `asset_history`
- **컬럼 구조 활용 및 보완**:
- `asset_id`: 대상 자산 식별자
- `event_type`: 변경 유형 (DEPT_CHANGE, USER_CHANGE, ROLE_CHANGE, STATUS_CHANGE)
- `details`: "상태 변경: 운영 -> 수리" 와 같이 읽기 쉬운 문자열 저장
- `cost`: 관련 비용 발생 시 기록 (수리비 등)
- `log_user`: 변경을 수행한 작업자
- `log_date`: 변경 발생 일시
### B. 백엔드 (Server-side Logic)
- **위치**: `server.js``POST /api/asset/:category/save` 엔드포인트
- **동작 흐름**:
1. **Snapshot**: 인서트/업데이트 수행 전, 기존 DB의 데이터를 `SELECT`하여 메모리에 저장.
2. **Comparison**: 요청된 신규 데이터와 기존 데이터를 필드별로 대조.
3. **Auto-logging**: 변경점이 발견되면 `asset_history` 테이블에 즉시 인서트.
4. **Transaction**: 모든 로그 생성이 자산 저장과 하나의 트랜잭션으로 묶여야 함.
### C. 프론트엔드 (UI/UX)
- **위치**: `HWModal.ts` 우측 `modal-history-area`
- **개선 사항**:
- `renderHistory()` 함수를 고도화하여 이벤트 타입별 아이콘/컬러 적용.
- "이전 값 ➔ 이후 값" 형태의 직관적인 레이아웃 도입.
- 스크롤을 통한 무제한 누적 이력 조회 지원.
## 4. 단계별 구현 로직
### 1단계: 서버 로직 고도화
- `server.js`에 비교 함수(`compareAndLog`) 구현.
- 각 자산 카테고리별 저장 로직에 비교 로직 삽입.
### 2단계: DB 데이터 마이그레이션 (필요시)
- 기존 자산의 `current_dept` 등을 `previous_dept`로 밀어내는 로직 점검.
### 3단계: UI 타임라인 렌더링 개선
- `modal.css`에 이력 전용 스타일(이벤트 뱃지 등) 추가.
- `HWModal.ts`에서 최신 로그를 실시간으로 다시 불러오는 로직 확인.
## 5. 검증 계획
- **자동 감지 테스트**: 상태 변경 후 저장 시 우측 이력에 즉시 한 줄이 추가되는지 확인.
- **다중 변경 테스트**: 조직과 사용자를 동시에 변경했을 때 두 개의 로그가 생성되는지 확인.
- **데이터 무결성**: 수정을 취소하거나 저장 실패 시 로그가 남지 않는지(Transaction) 확인.

View File

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

48
docs/plans/design_rule.md Normal file
View File

@@ -0,0 +1,48 @@
# 🎨 ITAM 시스템 디자인 가이드 (Design Guide)
본 문서는 ITAM(IT Asset Management System)의 시각적 일관성과 사용자 경험을 유지하기 위한 핵심 디자인 원칙을 정의합니다.
---
### 1. 디자인 철학 (Design Philosophy)
* **Minimalist & Stark**: Vercel 스타일의 극도로 간결하고 현대적인 디자인을 지향합니다.
* **Achromatic Base**: 블랙(#171717)과 화이트를 기본으로 하며, 정보의 구분은 얇은 헤어라인(#ebebeb)을 사용합니다.
* **Fluid & Responsive**: 고정된 픽셀 대신 화면 크기에 비례하여 UI 밀도가 변하는 유동적 스케일링 시스템을 적용합니다.
### 2. 타이포그래피 및 자간 (Typography & Letter-spacing)
* **Font Family**: `Pretendard` 단일 폰트를 사용합니다.
* **Letter-spacing**: 모든 텍스트에 `-0.02em` (-2%) 자간을 적용하여 밀도 있는 가독성을 확보합니다.
* **Typography Scale**:
* **XS**: `clamp(10px, 1.2vmin + 0.2vw, 15px)` - 보조 텍스트
* **SM**: `clamp(12px, 1.4vmin + 0.3vw, 18px)` - 필터, 일반 라벨, 테이블 헤더
* **Base**: `clamp(14px, 1.6vmin + 0.4vw, 22px)` - 본문, 테이블 데이터
* **MD**: `clamp(18px, 2.5vmin + 0.5vw, 30px)` - 섹션 소제목
* **LG**: `clamp(24px, 4vmin + 0.6vw, 48px)` - 페이지 대제목
* **XL**: `clamp(32px, 6vmin + 0.8vw, 72px)` - 핵심 통계 지표
* **Layout Units**:
* **Header Height**: `clamp(50px, 8vmin, 90px)`
* **Base Spacing**: `clamp(0.75rem, 3vmin, 3rem)`
* **Radius**: `clamp(6px, 1.5vmin, 16px)`
### 3. 컬러 팔레트 (Vercel Stark Palette)
* **Primary**: `#171717` (Stark Black) - 텍스트, 주요 버튼, 강조 요소.
* **Secondary**: `#888888` (Mute) - 보조 텍스트, 비활성 아이콘.
* **Border**: `#ebebeb` (Hairline) - 정보 구분선.
* **Background**: `#ffffff` (Canvas), `#fafafa` (Soft), `#f5f5f5` (Soft 2).
* **Accents**: Blue(`#0070f3`), Orange(`#f5a623`), Danger(`#ee0000`).
### 4. 컴포넌트 및 레이아웃 규칙 (Component Rules)
* **Header & Navigation**:
* 상단 1열 통합 바 형태를 유지하며, GNB와 LNB를 동일 라인에 배치하여 공간을 효율적으로 사용합니다.
* **Unified Filter Bar**:
* 검색창과 필터는 상단 타이틀 바로 아래(기존 액션 버튼 라인)까지 올려서 배치합니다.
* **Action Group**: '자산 추가', '부품 마스터' 등의 주요 액션 버튼은 검색창과 같은 라인의 최우측에 정렬합니다.
* **Dashboard**:
* **Single-Screen View**: 1920*1080(또는 1920*919) 해상도에서 스크롤 없이 한 화면에 핵심 정보가 모두 보이도록 최적화합니다.
* **Fixed Charts**: 차트 내부 숫자나 요소에 애니메이션(`animation: false`) 및 플로팅 레이블을 배제하여 정적인 안정성을 확보합니다.
* **Footer**:
* 화면 최하단에 위치하며, 텍스트는 **우측 정렬(Right-aligned)**합니다.
* 상단에 1px 헤어라인 구분선을 가집니다.
* **Security & UX**:
* **Text Selection**: 사용자의 실수에 의한 UI 드래그 방지를 위해 입력창(`input`, `textarea`)을 제외한 전체 영역의 텍스트 선택을 차단합니다.
* **View Toggle**: '서버' 탭 등 특정 탭에서만 '목록보기' 체크박스를 통해 뷰를 전환하며, 그 외 화면은 리스트 중심의 UI를 제공합니다.

View File

@@ -1,77 +1,77 @@
# H/W 자산관리 시스템 프로토타입 구현 계획 (Excel 기반 DB) # H/W 자산관리 시스템 프로토타입 구현 계획 (Excel 기반 DB)
현재 프로젝트는 DB 연동 없이 프로토타입으로 개발되며, 초기 H/W 자산을 엑셀 파일로 관리할 수 있도록 설계합니다. 지정된 디자인 가이드라인(`README.md`)에 따라 세련되고 전문적인 UI를 Vite + Vanilla JS (또는 TS) 기반으로 구축합니다. 현재 프로젝트는 DB 연동 없이 프로토타입으로 개발되며, 초기 H/W 자산을 엑셀 파일로 관리할 수 있도록 설계합니다. 지정된 디자인 가이드라인(`README.md`)에 따라 세련되고 전문적인 UI를 Vite + Vanilla JS (또는 TS) 기반으로 구축합니다.
## User Review Required ## User Review Required
> [!IMPORTANT] > [!IMPORTANT]
> **엑셀을 DB처럼 사용하는 방식**에 대한 주요 동작 흐름은 다음과 같습니다. 해당 방식이 의도하신 바와 맞는지 확인 부탁드립니다. > **엑셀을 DB처럼 사용하는 방식**에 대한 주요 동작 흐름은 다음과 같습니다. 해당 방식이 의도하신 바와 맞는지 확인 부탁드립니다.
> 1. 화면 진입 시 제공되는 **템플릿 엑셀 파일 다운로드** > 1. 화면 진입 시 제공되는 **템플릿 엑셀 파일 다운로드**
> 2. 해당 양식에 맞춰 데이터 입력 후 브라우저에 **엑셀 파일 업로드(Import)** > 2. 해당 양식에 맞춰 데이터 입력 후 브라우저에 **엑셀 파일 업로드(Import)**
> 3. 웹 상에서 **Data Table 형태로 렌더링** (편집/추가/삭제 기능 제공) > 3. 웹 상에서 **Data Table 형태로 렌더링** (편집/추가/삭제 기능 제공)
> 4. 수정이 완료된 후 **엑셀 파일로 다시 다운로드(Export)** 하여 로컬에 저장 > 4. 수정이 완료된 후 **엑셀 파일로 다시 다운로드(Export)** 하여 로컬에 저장
## 데이터 스키마 설계 (H/W 자산) ## 데이터 스키마 설계 (H/W 자산)
요청하신 항목에 맞춘 필수 H/W 자산 데이터 필드입니다. 엑셀의 열(Column)로 활용됩니다. 요청하신 항목에 맞춘 필수 H/W 자산 데이터 필드입니다. 엑셀의 열(Column)로 활용됩니다.
- **법인 (Company)**: 소속 법인 - **법인 (Company)**: 소속 법인
- **자산코드 (AssetCode)**: 자산의 고유 식별자 - **자산코드 (AssetCode)**: 자산의 고유 식별자
- **명칭 (DeviceName)**: 모델명 또는 기기명 - **명칭 (DeviceName)**: 모델명 또는 기기명
- **위치 (Location)**: 현재 파악된 물리적 위치 - **위치 (Location)**: 현재 파악된 물리적 위치
- **관리자 (Manager)**: 실 사용자 또는 담당자 - **관리자 (Manager)**: 실 사용자 또는 담당자
- **IP주소 (IPAddress)**: 할당된 IP - **IP주소 (IPAddress)**: 할당된 IP
- **MAC address (MacAddress)**: 기기 고유 MAC 주소 - **MAC address (MacAddress)**: 기기 고유 MAC 주소
- **H/W 사양 (HWSpecs)**: CPU, RAM, Storage 등 사양 요약 - **H/W 사양 (HWSpecs)**: CPU, RAM, Storage 등 사양 요약
- **OS (OperatingSystem)**: 설치된 운영체제 정보 - **OS (OperatingSystem)**: 설치된 운영체제 정보
## Proposed Changes ## Proposed Changes
### 1. 개발 환경 설정 (Vite 기반) ### 1. 개발 환경 설정 (Vite 기반)
- `npx create-vite` 를 사용하여 `vanilla-ts` (또는 `vanilla`) 프로젝트를 현재 디렉토리에 초기화합니다. - `npx create-vite` 를 사용하여 `vanilla-ts` (또는 `vanilla`) 프로젝트를 현재 디렉토리에 초기화합니다.
- `package.json``vite.config.ts` 설정 (포트 8080 및 host 허용) - `package.json``vite.config.ts` 설정 (포트 8080 및 host 허용)
- Excel 제어를 위해 `xlsx` (SheetJS) 라이브러리를 설치합니다. - Excel 제어를 위해 `xlsx` (SheetJS) 라이브러리를 설치합니다.
#### [NEW] package.json #### [NEW] package.json
#### [NEW] vite.config.ts #### [NEW] vite.config.ts
--- ---
### 2. UI 및 디자인 컴포넌트 (`README.md` 가이드라인 준수) ### 2. UI 및 디자인 컴포넌트 (`README.md` 가이드라인 준수)
- 디자인: Box-less Design, Line-based Division 적용 - 디자인: Box-less Design, Line-based Division 적용
- 컬러: `#1E5149`(Point), `#E5E7EB`(Border), `#F9FAFB`(Background) - 컬러: `#1E5149`(Point), `#E5E7EB`(Border), `#F9FAFB`(Background)
- 폰트: `Pretendard`, `Letter Spacing: -0.02em` 적용 - 폰트: `Pretendard`, `Letter Spacing: -0.02em` 적용
- 테이블 요소: 구분선만 사용하는 미니멀 테이블 - 테이블 요소: 구분선만 사용하는 미니멀 테이블
#### [NEW] index.css #### [NEW] index.css
--- ---
### 3. 메인 HTML 및 로직 구현 ### 3. 메인 HTML 및 로직 구현
- **File Upload Area**: 엑셀 파일을 불러오거나 템플릿을 다운로드 할 수 있는 상단 컨트롤 영역. - **File Upload Area**: 엑셀 파일을 불러오거나 템플릿을 다운로드 할 수 있는 상단 컨트롤 영역.
- **Data Table**: 파싱된 H/W 자산 리스트를 출력. - **Data Table**: 파싱된 H/W 자산 리스트를 출력.
- **Modal Component**: `README.md`에 정의된 2열 그리드와 우측 상단 닫기, 하단 저장 버튼이 포함된 정보 수정/생성 모달. - **Modal Component**: `README.md`에 정의된 2열 그리드와 우측 상단 닫기, 하단 저장 버튼이 포함된 정보 수정/생성 모달.
- **Excel Logic**: 업로드된 Excel 데이터를 파싱하여 JSON 형태로 브라우저 메모리에 들고 처리한 후 다시 Excel로 내보내는 기능. - **Excel Logic**: 업로드된 Excel 데이터를 파싱하여 JSON 형태로 브라우저 메모리에 들고 처리한 후 다시 Excel로 내보내는 기능.
#### [NEW] index.html #### [NEW] index.html
#### [NEW] src/main.ts #### [NEW] src/main.ts
#### [NEW] src/excelHandler.ts #### [NEW] src/excelHandler.ts
## Open Questions ## Open Questions
> [!WARNING] > [!WARNING]
> 1. 프론트엔드 프레임워크 강제 규정이 없다면, 경량화 및 설정 편의를 위해 `Vite + Vanilla TypeScript` 를 사용하는 것이 괜찮으신가요? (원하신다면 React나 Vue로도 가능합니다.) > 1. 프론트엔드 프레임워크 강제 규정이 없다면, 경량화 및 설정 편의를 위해 `Vite + Vanilla TypeScript` 를 사용하는 것이 괜찮으신가요? (원하신다면 React나 Vue로도 가능합니다.)
> 2. 추가적인 검색 필터(예: 법인별, 관리자별 검색)가 당장 도입되어야 하는 필수 기능인가요? > 2. 추가적인 검색 필터(예: 법인별, 관리자별 검색)가 당장 도입되어야 하는 필수 기능인가요?
## Verification Plan ## Verification Plan
### Automated Tests ### Automated Tests
- `npm run dev` 를 통해 `http://localhost:8080` 포트 개방 성공 여부 확인. - `npm run dev` 를 통해 `http://localhost:8080` 포트 개방 성공 여부 확인.
- 브라우저 기능 테스트: - 브라우저 기능 테스트:
- 템플릿 다운로드 클릭 -> 정상적인 `hw_assets.xlsx` 다운로드 여부 - 템플릿 다운로드 클릭 -> 정상적인 `hw_assets.xlsx` 다운로드 여부
- 샘플 엑셀 파일 업로드 -> 데이터 테이블에 행(Row) 생성 여부 - 샘플 엑셀 파일 업로드 -> 데이터 테이블에 행(Row) 생성 여부
- 항목 더블 클릭 혹은 [수정] 버튼 클릭 시 -> 모달 팝업 및 데이터 연동 확인 - 항목 더블 클릭 혹은 [수정] 버튼 클릭 시 -> 모달 팝업 및 데이터 연동 확인
- [저장 후 내보내기] 클릭 -> 업데이트된 데이터가 포함된 새 엑셀 파일 다운로드 확인 - [저장 후 내보내기] 클릭 -> 업데이트된 데이터가 포함된 새 엑셀 파일 다운로드 확인
### Manual Verification ### Manual Verification
- 디자인 요구사항(`Pretendard`, `#1E5149`, Border-less 컨셉) 반영 확인을 스크린샷 렌더링으로 사용자와 상호작용합니다. - 디자인 요구사항(`Pretendard`, `#1E5149`, Border-less 컨셉) 반영 확인을 스크린샷 렌더링으로 사용자와 상호작용합니다.

View File

@@ -1,72 +1,72 @@
# HM ITAM (IT Asset Management) ERP 기능 명세서 # HM ITAM (IT Asset Management) ERP 기능 명세서
## 1. 개요 (Overview) ## 1. 개요 (Overview)
본 시스템은 데이터베이스(DB) 연결 없이 브라우저 단에서 엑셀(Excel) 파일을 로컬 데이터베이스의 대체재로 활용하여 구동되는 **IT 자산관리(H/W, S/W) 프로토타입 대시보드**입니다. 사용자는 별도의 백엔드 없이 엑셀 파일을 업로드하고, 웹 상에서 조회 및 수정 후 다시 엑셀로 저장(Export)할 수 있습니다. 본 시스템은 데이터베이스(DB) 연결 없이 브라우저 단에서 엑셀(Excel) 파일을 로컬 데이터베이스의 대체재로 활용하여 구동되는 **IT 자산관리(H/W, S/W) 프로토타입 대시보드**입니다. 사용자는 별도의 백엔드 없이 엑셀 파일을 업로드하고, 웹 상에서 조회 및 수정 후 다시 엑셀로 저장(Export)할 수 있습니다.
## 2. 전체 레이아웃 (Layout & Navigation) ## 2. 전체 레이아웃 (Layout & Navigation)
화면은 좌측 사이드바 구조(Depth 2)를 채택하여 정보 탐색의 편의성을 고려하였습니다. 화면은 좌측 사이드바 구조(Depth 2)를 채택하여 정보 탐색의 편의성을 고려하였습니다.
### 2.1. 좌측 메인 내비게이션 (Sidebar) ### 2.1. 좌측 메인 내비게이션 (Sidebar)
* **하드웨어 (H/W)**: 대시보드, 개인PC, 서버, 스토리지, 전산비품 * **하드웨어 (H/W)**: 대시보드, 개인PC, 서버, 스토리지, 전산비품
* **소프트웨어 (S/W)**: 대시보드, 구독 소프트웨어, 영구 소프트웨어 * **소프트웨어 (S/W)**: 대시보드, 구독 소프트웨어, 영구 소프트웨어
### 2.2. 우측 메인 영역 (Main Content) ### 2.2. 우측 메인 영역 (Main Content)
* **상단 컨트롤 패널**: 좌측 탭에 상관없이 데이터를 일괄 제어하는 통합 엑셀 버튼 3종(`통합 양식 다운로드`, `엑셀 업로드`, `일괄 엑셀 저장`)이 위치합니다. * **상단 컨트롤 패널**: 좌측 탭에 상관없이 데이터를 일괄 제어하는 통합 엑셀 버튼 3종(`통합 양식 다운로드`, `엑셀 업로드`, `일괄 엑셀 저장`)이 위치합니다.
* **타이틀 바**: 사용자가 현재 어느 탭(ex. `하드웨어 / 개인PC`)에 위치하고 있는지 동적으로 표시합니다. * **타이틀 바**: 사용자가 현재 어느 탭(ex. `하드웨어 / 개인PC`)에 위치하고 있는지 동적으로 표시합니다.
* **콘텐츠 뷰**: 대시보드 선택 시 각 자산의 요약 맵(Summary Grid)을, 하위 자산 항목 선택 시 Data Table을 렌더링합니다. * **콘텐츠 뷰**: 대시보드 선택 시 각 자산의 요약 맵(Summary Grid)을, 하위 자산 항목 선택 시 Data Table을 렌더링합니다.
--- ---
## 3. 핵심 기능 (Core Features) ## 3. 핵심 기능 (Core Features)
### 3.1. 엑셀 연동 기반 CRUD 로직 파이프라인 ### 3.1. 엑셀 연동 기반 CRUD 로직 파이프라인
* **통합 양식 다운로드 (Template Export)**: 자산이 없는 경우, 초기 세팅을 돕기 위해 빈 엑셀 템플릿(Master File)을 다운로드할 수 있습니다. 다운로드된 파일은 **다중 시트(Multi-sheet)**`개인PC`, `서버`, `스토리지`, `전산비품`, `구독SW`, `영구SW` 6개의 탭이 분리 생성됩니다. * **통합 양식 다운로드 (Template Export)**: 자산이 없는 경우, 초기 세팅을 돕기 위해 빈 엑셀 템플릿(Master File)을 다운로드할 수 있습니다. 다운로드된 파일은 **다중 시트(Multi-sheet)**`개인PC`, `서버`, `스토리지`, `전산비품`, `구독SW`, `영구SW` 6개의 탭이 분리 생성됩니다.
* **엑셀 업로드 (Import/Parse)**: `SheetJS (xlsx)` 라이브러리를 통해 다중 시트 형태의 엑셀 파일을 업로드하면, 한 번에 브라우저 내의 자산 리스트 객체(Array)로 매핑되어 각 탭에 뿌려집니다. * **엑셀 업로드 (Import/Parse)**: `SheetJS (xlsx)` 라이브러리를 통해 다중 시트 형태의 엑셀 파일을 업로드하면, 한 번에 브라우저 내의 자산 리스트 객체(Array)로 매핑되어 각 탭에 뿌려집니다.
* **자산 조회 (Read)**: 각 자산 항목(ex. 서버) 탭을 클릭하여 들어가면, 해당 시트(Type)에 속한 자산 목록만 필터링되어 테이블로 노출됩니다. * **자산 조회 (Read)**: 각 자산 항목(ex. 서버) 탭을 클릭하여 들어가면, 해당 시트(Type)에 속한 자산 목록만 필터링되어 테이블로 노출됩니다.
* **자산 추가/수정 (Create/Update)**: 테이블 우측의 `[수정]` 버튼 또는 상단의 `[자산 추가]`를 클릭하면 모달 팝업이 등장하여 H/W와 S/W에 맞는 각기 다른 양식 폼 데이터를 브라우저 메모리에 업데이트합니다. * **자산 추가/수정 (Create/Update)**: 테이블 우측의 `[수정]` 버튼 또는 상단의 `[자산 추가]`를 클릭하면 모달 팝업이 등장하여 H/W와 S/W에 맞는 각기 다른 양식 폼 데이터를 브라우저 메모리에 업데이트합니다.
* **자산 삭제 (Delete)**: 모달 팝업 좌측 하단의 `[삭제]` 버튼을 통해 해당 단일 항목을 삭제할 수 있습니다. * **자산 삭제 (Delete)**: 모달 팝업 좌측 하단의 `[삭제]` 버튼을 통해 해당 단일 항목을 삭제할 수 있습니다.
* **일괄 엑셀 저장 (Save/Export)**: 모든 추가/수정/삭제 작업이 완료되면 버튼을 눌러 변경된 전체 메모리 데이터를 다시 **다중 시트 엑셀 파일** 형태로 로컬 PC에 떨굽니다. * **일괄 엑셀 저장 (Save/Export)**: 모든 추가/수정/삭제 작업이 완료되면 버튼을 눌러 변경된 전체 메모리 데이터를 다시 **다중 시트 엑셀 파일** 형태로 로컬 PC에 떨굽니다.
### 3.2. 대시보드 (Dashboard) ### 3.2. 대시보드 (Dashboard)
* **H/W 대시보드**: 개인PC, 서버, 스토리지, 전산비품의 총 수량을 Grid 기반 카드로 요약하여 보여줍니다. * **H/W 대시보드**: 개인PC, 서버, 스토리지, 전산비품의 총 수량을 Grid 기반 카드로 요약하여 보여줍니다.
* **S/W 대시보드**: 구독 소프트웨어, 영구 소프트웨어의 총 라이선스 개수를 요약하여 보여줍니다. * **S/W 대시보드**: 구독 소프트웨어, 영구 소프트웨어의 총 라이선스 개수를 요약하여 보여줍니다.
--- ---
## 4. 데이터 스키마 (Data Schema) ## 4. 데이터 스키마 (Data Schema)
자산 항목은 H/W와 S/W 두 가지의 다른 구조로 관리됩니다. 자산 항목은 H/W와 S/W 두 가지의 다른 구조로 관리됩니다.
### 4.1. H/W 자산 스키마 (Hardware Asset) ### 4.1. H/W 자산 스키마 (Hardware Asset)
`[개인PC, 서버, 스토리지, 전산비품]` 각각 동일한 스키마 구조를 가집니다. `[개인PC, 서버, 스토리지, 전산비품]` 각각 동일한 스키마 구조를 가집니다.
| 필드명 | 유형 (Type) | 필수여부 | 설명 | | 필드명 | 유형 (Type) | 필수여부 | 설명 |
| :--- | :---: | :---: | :--- | | :--- | :---: | :---: | :--- |
| **법인** | `String` | 필 | 자산의 소속 법인 | | **법인** | `String` | 필 | 자산의 소속 법인 |
| **자산코드** | `String` | 필 | 고유 자산 식별코드 | | **자산코드** | `String` | 필 | 고유 자산 식별코드 |
| **명칭** | `String` | 필 | 모델명 또는 기기명 | | **명칭** | `String` | 필 | 모델명 또는 기기명 |
| **위치** | `String` | 선 | 물리적 위치 (ex. 개발실) | | **위치** | `String` | 선 | 물리적 위치 (ex. 개발실) |
| **관리자** | `String` | 선 | 실 사용자 또는 책임자 | | **관리자** | `String` | 선 | 실 사용자 또는 책임자 |
| **IP주소** | `String` | 선 | 할당된 고정/유동 IP | | **IP주소** | `String` | 선 | 할당된 고정/유동 IP |
| **MAC address** | `String` | 선 | 기기 고유 물리적 주소 | | **MAC address** | `String` | 선 | 기기 고유 물리적 주소 |
| **OS** | `String` | 선 | 설치된 운영체제 정보 | | **OS** | `String` | 선 | 설치된 운영체제 정보 |
| **H/W 사양** | `String` | 선 | CPU, RAM, Storage 요약 스펙 | | **H/W 사양** | `String` | 선 | CPU, RAM, Storage 요약 스펙 |
### 4.2. S/W 자산 스키마 (Software Asset) ### 4.2. S/W 자산 스키마 (Software Asset)
`[구독SW, 영구SW]` 각각 동일한 스키마 구조를 가집니다. `[구독SW, 영구SW]` 각각 동일한 스키마 구조를 가집니다.
| 필드명 | 유형 (Type) | 필수여부 | 설명 | | 필드명 | 유형 (Type) | 필수여부 | 설명 |
| :--- | :---: | :---: | :--- | | :--- | :---: | :---: | :--- |
| **법인** | `String` | 필 | 자산의 소속 법인 | | **법인** | `String` | 필 | 자산의 소속 법인 |
| **S/W명** | `String` | 필 | 소프트웨어 제품 명칭 | | **S/W명** | `String` | 필 | 소프트웨어 제품 명칭 |
| **라이선스키** | `String` | 선 | 발급된 S/W 활성화 키 | | **라이선스키** | `String` | 선 | 발급된 S/W 활성화 키 |
| **할당자** | `String` | 선 | 사용하는 사용자 또는 팀 | | **할당자** | `String` | 선 | 사용하는 사용자 또는 팀 |
| **사용기간** | `String` | 선 | 구독 혹은 만료 기한 표기 | | **사용기간** | `String` | 선 | 구독 혹은 만료 기한 표기 |
| **비고** | `String` | 선 | 기타 참고용 안내 사항 | | **비고** | `String` | 선 | 기타 참고용 안내 사항 |
--- ---
## 5. UI/UX 디자인 정책 (Design Constraint) ## 5. UI/UX 디자인 정책 (Design Constraint)
1. **Color (Achromatic & Green)**: `Deep Green(#1E5149)` 을 메인 포인트 색상으로 사용하여 전문성과 정돈된 느낌을 줍니다. 배경이나 나머지 요소는 무채색 베이스입니다. 1. **Color (Achromatic & Green)**: `Deep Green(#1E5149)` 을 메인 포인트 색상으로 사용하여 전문성과 정돈된 느낌을 줍니다. 배경이나 나머지 요소는 무채색 베이스입니다.
2. **Typography**: 가독성이 우수한 `Pretendard` 서체를 사용하고, 자간을 약간 좁혀 밀도 있고 깔끔한 느낌을 줍니다. 2. **Typography**: 가독성이 우수한 `Pretendard` 서체를 사용하고, 자간을 약간 좁혀 밀도 있고 깔끔한 느낌을 줍니다.
3. **Box-less 스타일링**: 과도한 박스와 테두리를 없애고(Border-based), 얇은 구분 영역만으로 테이블과 폼의 요소를 분리하여 세련된 데이터 표현을 만듭니다. 3. **Box-less 스타일링**: 과도한 박스와 테두리를 없애고(Border-based), 얇은 구분 영역만으로 테이블과 폼의 요소를 분리하여 세련된 데이터 표현을 만듭니다.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -1,51 +0,0 @@
# 구조 개선 및 다중 탭(Depth 2) 도입 계획
사용자 요청에 따라 H/W와 S/W를 구분하고, 그 하위에 각각 대시보드 및 상세 항목(개인PC, 서버 등) 탭을 나누는 네비게이션 구조를 도입합니다. 바닐라 JS 기반에서 각 탭마다 다른 데이터 테이블을 그려내는 아키텍처로 개선합니다.
## User Review Required
> [!IMPORTANT]
> 1. **엑셀 관리 방식 (Sheets 분리)**: 단일 엑셀 파일 안에 여러 개의 시트(Sheet)를 나누어 관리하는 방식으로 제안합니다. 한 번 엑셀을 업로드하면, `개인PC`, `서버`, `스토리지`, `전산비품` 등 각각의 시트를 한방에 파싱하여 각 탭에 적용하도록 구성하겠습니다.
> 2. **S/W 스키마**: 현재 H/W 기반 데이터 스키마만 정의되어 있습니다. [구독 소프트웨어]와 [영구 소프트웨어] 탭 개발을 위한 데이터 항목들(예: 사용기간, 라이선스키, 결제방식 등)은 아직 정해지지 않았으므로 일단 공통 S/W 데이터 스키마 임시 템플릿(S/W명, 유형, 라이선스키, 할당된 사용자 등)으로 만들어 두고 추후 수정할 수 있도록 개발해도 될까요?
## Proposed Changes
### 1. UI/UX: 2 Depth 네비게이션 (`index.html`, `style.css`)
- **좌측(또는 상단) GNB (Global Navigation Bar)**: H/W 와 S/W 를 스위치할 수 있는 메인 탭 생성.
- **LNB (Local Navigation Bar)**: 메인 탭 전환 시 나타나는 서브 탭(H/W: 대시보드/PC/서버/스토리지/비품, S/W: 대시보드/구독/영구).
- `README.md` 가이드라인에 따라 화면을 분할하고 정보 밀도를 높이기 위해 Box-less, Line-based Layout 유지.
#### [MODIFY] index.html
#### [MODIFY] src/style.css
---
### 2. 다중 데이터 구조 및 상태 관리 (`main.ts`)
- 현재 선택된 메뉴 뎁스(예: `activeCategory = 'HW'`, `activeSubTab = '개인PC'`)에 따라 렌더링 함수가 동기화되도록 라우팅/상태 관리 로직 추가.
- `Dashboard` 탭 진입 시, 모든 서브 탭 데이터의 갯수(Total PCs, Total Servers 등)를 한눈에 볼 수 있는 요약 영역(Summary Cards/Charts 영역) 예약 및 구현.
#### [MODIFY] src/main.ts
---
### 3. 멀티-시트(Multi-sheet) 엑셀 파싱 (`excelHandler.ts`)
- `SheetJS` 기능을 확장하여 다운로드/데이터 추출 시 다중 시트 생성.
- **H/W 템플릿 시트명**: `[개인PC, 서버, 스토리지, 전산비품]`
- **S/W 템플릿 시트명**: `[구독SW, 영구SW]`
#### [MODIFY] src/excelHandler.ts
## Open Questions
> [!WARNING]
> * 왼쪽 사이드바로 메뉴를 구성하는 것이 좋을까요, 상단 가로바(Top Nav) 2단으로 구성하는 것이 좋을까요? Reference 이미지가 따로 없다면 범용적으로 관리하기 편한 **왼쪽 사이드바 구조(Sidebar Menu)** 를 제안합니다. (진행 승인 시 사이드바 형태로 구현합니다.)
## Verification Plan
### Automated Tests
- 좌측 `H/W`, `S/W` 클릭 시 서브 메뉴가 정상 토글되는지 검증(`main.ts` DOM class toggle 확인).
- 서브 메뉴 `서버` 클릭 시 빈 테이블(또는 서버 자산 테이블)이 그려지는지 확인.
- 달라진 구조로 `엑셀 템플릿 양식`을 다운로드했을 때 파일에 다수의 시트(Sheet)가 정상 분류되어 있는지 확인.
### Manual Verification
- 브라우저 에이전트를 통해 바뀐 화면의 스크린샷(LNB 사이드바, Dashboard 화면 등)을 찍어 사용자에게 보고.

View File

@@ -1,47 +0,0 @@
# 임시 DB 생성 및 S/W 사용자 관리 개편
임시 DB 엑셀 파일 생성과 S/W 목록의 '할당자' 속성 UI 개편에 대한 기술 구현 계획입니다.
## User Review Required
> [!IMPORTANT]
> **사용자 관리 데이터 저장 방식에 대한 피드백이 필요합니다.**
> 엑셀을 임시 DB로 사용하고 있기 때문에, "사용자 관리" 팝업에서 추가/삭제된 사용자 목록을 엑셀에 저장할 때 **쉼표(,)로 구분된 하나의 문자열**(예: `홍길동, 김철수, 이영희`)로 기존 `할당자` 컬럼에 업데이트 하는 방식을 제안합니다. 이 방식이 괜찮으신가요?
## Proposed Changes
### 1. 임시 DB 연동
임시로 사용할 초기 엑셀 파일(`temp_db.xlsx`)을 프로젝트 루트에 스크립트를 통해 생성합니다.
- 개인PC, 서버, 구독SW, 영구SW 시트에 각각 구성을 확인할 수 있는 dummy 데이터 1~2개씩을 포함하여 생성합니다.
- 향후 화면에서 '엑셀 업로드'를 통해 이 파일을 업로드하여 데이터를 화면에 뿌려볼 수 있습니다. (원하시면 페이지 로드 시 이 파일을 임포트하도록 로직을 변경할 수도 있으나, 브라우저 단에서 로컬 파일을 자동 리딩하는 것은 제한이 있으므로 기본적으로는 파일을 제공만 합니다.)
---
### 2. 컴포넌트: HTML 구조 변경
#### [MODIFY] [index.html](file:///c:/Project/HM%20ITAM/index.html)
- `sw-asset-modal`의 폼 내용 중 "할당자" 입력 폼(<label> 및 <input>) 제거
- 관리 팝업을 위한 `sw-user-modal` 모달 오버레이 마크업 추가
기존 유저 목록을 보여주고, 새 사용자를 추가하거나 기존 사용자를 삭제할 수 있는 UI (리스트, 추가 인풋, 추가 버튼 기반) 작성
---
### 3. 컴포넌트: 로직 및 스타일
#### [MODIFY] [src/main.ts](file:///c:/Project/HM%20ITAM/src/main.ts)
- S/W 렌더링 영역(`renderTable`)에서 데스크탑 뷰의 `<th>할당자</th>` 및 해당하는 셀(`<td>`) 제거
- S/W `관리` 탭(`<td>`)에 수정 버튼(`btn-edit`) 옆에 사용자 관리 아이콘 (Lucide의 `Users` 또는 `UserCog` 아이콘 활용) 추가
- 사용자 관리 아이콘 클릭 시 `sw-user-modal` 팝업 띄우는 이벤트 리스너 추가
- `sw-user-modal` 팝업 내에서 사용자를 추가/삭제하고 '저장' 시, 해당 S/W 자산의 `할당자` 데이터를 갱신하도록 처리 (쉼표 구분 형태)
#### [MODIFY] [src/excelHandler.ts](file:///c:/Project/HM%20ITAM/src/excelHandler.ts)
- (선택 사항) `SW_HEADERS`나 엑셀 파싱 로직은 그대로 두어 하위 호환성 유지. 사용자가 데이터를 쉼표 형태로 주고 받을 것이므로 별도의 인터페이스 변경은 없음.
## Open Questions
- 사용자 관리 팝업에서 저장할 때, 이름 말고 '부서'나 '직급' 같은 추가적인 정보도 관리가 필요하신가요? (기본적으로는 엑셀에 단일 텍스트로 보존되므로 '이름'만 관리하는 것으로 설계했습니다.)
- 개발 환경(Vite)에서 초기 로딩 시 `temp_db.xlsx`를 자동으로 불러오도록 Vite의 플러그인 또는 fetch 로직을 추가하는 것을 원하시나요? 아니면 엑셀 파일만 만들어 드리고 사용자가 '엑셀 업로드' 버튼으로 직접 연동해 쓰는 방식이 좋으신가요?
## Verification Plan
### Manual Verification
1. `npm run dev` 후 브라우저 접속
2. 프로젝트 폴더에 `temp_db.xlsx` 파일이 생성되었는지 확인
3. 소프트웨어 > 영구/구독 탭 진입 시 "할당자" 테이블 헤더가 사라진 것 확인
4. 관리 탭의 "사용자 관리" 아이콘 클릭 시, 해당 소프트웨어의 사용자를 등록하고 삭제할 수 있는 팝업 등장하는지 확인
5. 사용자 아이콘을 클릭해 홍길동, 김철수 등록 후, 전체 엑셀 저장 혹은 다운로드 시 엑셀 파일 내의 '할당자' 열에 `홍길동,김철수` 로 잘 들어가는지 확인

View File

@@ -5,26 +5,22 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ITAM 자산관리 ERP</title> <title>한맥가족 자산관리시스템</title>
<link rel="stylesheet" <link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" /> href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" />
<link rel="stylesheet" href="/src/styles/common.css" />
<link rel="stylesheet" href="/src/styles/guide.css" />
<link rel="stylesheet" href="/src/styles/modal.css" />
<link rel="stylesheet" href="/src/styles/dashboard.css" />
<link rel="stylesheet" href="/src/styles/table.css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<script src="/qrcode.min.js"></script>
</head> </head>
<body> <body>
<div class="app-layout"> <div class="app-layout" id="app-layout" style="display: none;">
<!-- Single-Line Integrated Header --> <!-- Single-Line Integrated Header -->
<header class="main-header"> <header class="main-header">
<div class="header-container" id="nav-container"> <div class="header-container" id="nav-container">
<div class="brand"> <div class="brand">
<img src="/image 92.png" alt="Logo" class="main-logo" /> <img src="/image 92.png" alt="Logo" class="main-logo" />
<h1>자산관리시스템<span class="sub-title">(Digital Asset Control Hub System)</span></h1> <h1>한맥자산관리시스템</h1>
</div> </div>
<!-- Navigation (GNB + LNB in same row) --> <!-- Navigation (GNB + LNB in same row) -->
@@ -33,6 +29,14 @@
</nav> </nav>
<div class="header-actions"> <div class="header-actions">
<div class="role-switcher" id="role-switcher">
<span class="role-label user active">실무자</span>
<label class="switch">
<input type="checkbox" id="role-toggle-checkbox">
<span class="slider round"></span>
</label>
<span class="role-label admin">관리자</span>
</div>
<button id="btn-admin-page" class="hidden"></button> <!-- JS 호환용 숨김 --> <button id="btn-admin-page" class="hidden"></button> <!-- JS 호환용 숨김 -->
<button id="btn-open-guide-header" class="btn btn-outline" title="프로세스 가이드"> <button id="btn-open-guide-header" class="btn btn-outline" title="프로세스 가이드">
<i data-lucide="book-open"></i> 가이드 <i data-lucide="book-open"></i> 가이드
@@ -48,8 +52,7 @@
<!-- Footer --> <!-- Footer -->
<footer class="main-footer"> <footer class="main-footer">
<div id="secret-cloud-trigger" style="width: 20px; height: 20px; cursor: pointer; opacity: 0.1; background: #000; border-radius: 4px; position: absolute; left: 1rem;"></div> <p>&copy; 2026 BARON Consultant Co,Ltd. All rights reserved.</p>
<p>Powered by BARON Consultant Co,Ltd</p>
</footer> </footer>
</div> </div>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
label/LabelPrinter.exe Normal file

Binary file not shown.

BIN
label/Newtonsoft.Json.dll Normal file

Binary file not shown.

BIN
label/WebQuery.dll Normal file

Binary file not shown.

4
label/config.ini Normal file
View File

@@ -0,0 +1,4 @@
[PRINT]
FONT=8
LEFT=143
TOP=40

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

7
label/tmp/file_1.txt Normal file
View File

@@ -0,0 +1,7 @@
자산번호 : 210312
자산명 : 가을-PC(i5-12400F)
공급사 : (주)가을디에스
자산위치 : 지반부
관리부서 : 전산
사용자 : 박노석
취득일자 : 2024-08-05

Binary file not shown.

742
map_config.json Normal file
View File

@@ -0,0 +1,742 @@
{
"img/location_photo/IDC/서관205.png": [
{
"x": "50.78",
"y": "1.53",
"w": "46.10",
"h": "6.27",
"asset_id": null
},
{
"x": "50.78",
"y": "10.35",
"w": "46.10",
"h": "6.27",
"asset_id": null
},
{
"x": "50.78",
"y": "19.06",
"w": "46.10",
"h": "6.50",
"asset_id": null
},
{
"x": "50.78",
"y": "27.89",
"w": "46.10",
"h": "6.50",
"asset_id": "server_1779761946023_14"
},
{
"x": "50.78",
"y": "36.71",
"w": "46.10",
"h": "6.50",
"asset_id": "server_1779761946023_18"
},
{
"x": "50.78",
"y": "45.64",
"w": "46.10",
"h": "6.32",
"asset_id": "server_1779761946023_23"
},
{
"x": "50.78",
"y": "54.25",
"w": "46.10",
"h": "6.54",
"asset_id": "server_1779761946023_24"
},
{
"x": "50.78",
"y": "63.29",
"w": "46.10",
"h": "6.50",
"asset_id": "server_1779761946023_1"
},
{
"x": "50.78",
"y": "72.00",
"w": "46.10",
"h": "6.32",
"asset_id": "server_1779761946023_21"
},
{
"x": "50.78",
"y": "81.92",
"w": "18.40",
"h": "15.58",
"asset_id": "server_1779761946023_17"
},
{
"x": "78.62",
"y": "81.92",
"w": "18.31",
"h": "15.58",
"asset_id": "server_1779761946023_20"
}
],
"img/location_photo/IDC/서관202.png": [
{
"x": "56.35",
"y": "64.02",
"w": "40.87",
"h": "6.24",
"asset_id": "server_1779761946023_9"
},
{
"x": "56.35",
"y": "71.57",
"w": "40.87",
"h": "6.24",
"asset_id": "server_1779761946023_10"
},
{
"x": "56.35",
"y": "79.17",
"w": "40.87",
"h": "6.24",
"asset_id": "server_1779761946023_26"
},
{
"x": "56.35",
"y": "86.66",
"w": "40.87",
"h": "6.24",
"asset_id": "server_1779761946023_8"
},
{
"x": "56.35",
"y": "32.01",
"w": "40.87",
"h": "6.24",
"asset_id": "9pvkqyi"
}
],
"img/location_photo/IDC/서관203.png": [
{
"x": "56.07",
"y": "2.54",
"w": "41.11",
"h": "6.52",
"asset_id": null
},
{
"x": "56.07",
"y": "10.12",
"w": "41.11",
"h": "6.52",
"asset_id": null
},
{
"x": "56.07",
"y": "17.80",
"w": "41.11",
"h": "6.52",
"asset_id": null
},
{
"x": "56.07",
"y": "63.51",
"w": "41.11",
"h": "6.52",
"asset_id": null
},
{
"x": "56.07",
"y": "71.19",
"w": "41.11",
"h": "6.52",
"asset_id": null
},
{
"x": "56.07",
"y": "87.70",
"w": "41.11",
"h": "6.52",
"asset_id": "server_1779761946023_25"
}
],
"img/location_photo/IDC/서관204.png": [
{
"x": "48.87",
"y": "2.73",
"w": "47.80",
"h": "6.27",
"asset_id": null
},
{
"x": "48.87",
"y": "10.38",
"w": "47.80",
"h": "6.27",
"asset_id": "server_1779761946023_3"
},
{
"x": "48.87",
"y": "17.93",
"w": "47.80",
"h": "6.50",
"asset_id": "server_1779761946023_6"
},
{
"x": "48.87",
"y": "25.49",
"w": "47.80",
"h": "6.50",
"asset_id": null
},
{
"x": "48.87",
"y": "33.17",
"w": "47.80",
"h": "6.50",
"asset_id": "server_1779761946023_5"
},
{
"x": "48.87",
"y": "40.59",
"w": "47.80",
"h": "6.50",
"asset_id": "server_1779761946023_4"
},
{
"x": "48.87",
"y": "48.40",
"w": "47.80",
"h": "6.50",
"asset_id": null
},
{
"x": "48.87",
"y": "55.95",
"w": "47.80",
"h": "6.50",
"asset_id": "server_1779761946023_19"
},
{
"x": "48.87",
"y": "63.63",
"w": "47.80",
"h": "6.50",
"asset_id": "server_1779761946023_2"
},
{
"x": "48.87",
"y": "71.06",
"w": "47.80",
"h": "6.50",
"asset_id": "server_1779761946023_0"
},
{
"x": "48.87",
"y": "78.74",
"w": "47.80",
"h": "6.50",
"asset_id": "server_1779761946023_7"
},
{
"x": "48.87",
"y": "86.68",
"w": "18.99",
"h": "12.62",
"asset_id": "server_1779761946023_29"
}
],
"img/location_photo/IDC/동관53.png": [
{
"x": "61.62",
"y": "3.08",
"w": "35.96",
"h": "7.90",
"asset_id": "server_1779761946023_13"
},
{
"x": "61.62",
"y": "12.68",
"w": "35.96",
"h": "7.90",
"asset_id": "server_1779761946023_15"
},
{
"x": "61.62",
"y": "21.75",
"w": "35.96",
"h": "7.90",
"asset_id": "server_1779761946023_22"
}
],
"img/location_photo/IDC/동관54.png": [
{
"x": "54.71",
"y": "2.57",
"w": "42.42",
"h": "6.50",
"asset_id": null
},
{
"x": "54.71",
"y": "10.38",
"w": "42.42",
"h": "6.50",
"asset_id": null
},
{
"x": "54.71",
"y": "27.15",
"w": "42.42",
"h": "6.62",
"asset_id": "server_1779761946023_12"
},
{
"x": "54.71",
"y": "43.54",
"w": "42.42",
"h": "6.50",
"asset_id": "server_1779761946023_11"
},
{
"x": "54.71",
"y": "54.93",
"w": "42.42",
"h": "6.50",
"asset_id": null
},
{
"x": "54.71",
"y": "70.16",
"w": "42.42",
"h": "6.50",
"asset_id": "server_1779761946023_27"
},
{
"x": "54.71",
"y": "79.51",
"w": "42.42",
"h": "6.50",
"asset_id": "server_1779761946023_28"
}
],
"img/location_photo/한맥빌딩/MDF실/MDF_1.png": [
{
"x": "49.33",
"y": "14.99",
"w": "7.35",
"h": "11.22",
"asset_id": "cdp0e0c"
},
{
"x": "59.23",
"y": "14.99",
"w": "7.35",
"h": "11.22",
"asset_id": "emys9gb"
},
{
"x": "69.22",
"y": "14.99",
"w": "7.35",
"h": "11.22",
"asset_id": "vmbv3pj"
},
{
"x": "79.12",
"y": "14.99",
"w": "7.35",
"h": "11.22",
"asset_id": "4fysk40"
},
{
"x": "88.97",
"y": "14.99",
"w": "7.35",
"h": "11.22",
"asset_id": "x6jaehn"
},
{
"x": "48.57",
"y": "34.11",
"w": "7.52",
"h": "11.44",
"asset_id": "t87p0l0"
},
{
"x": "56.80",
"y": "34.11",
"w": "7.52",
"h": "11.44",
"asset_id": "ywosxiv"
},
{
"x": "64.94",
"y": "34.11",
"w": "7.52",
"h": "11.44",
"asset_id": null
},
{
"x": "72.89",
"y": "34.11",
"w": "7.56",
"h": "11.44",
"asset_id": null
},
{
"x": "81.22",
"y": "34.06",
"w": "7.52",
"h": "11.44",
"asset_id": null
},
{
"x": "89.36",
"y": "34.06",
"w": "7.52",
"h": "11.44",
"asset_id": "tormk2l"
},
{
"x": "48.57",
"y": "53.06",
"w": "9.06",
"h": "20.99",
"asset_id": null
},
{
"x": "58.48",
"y": "53.06",
"w": "9.06",
"h": "20.99",
"asset_id": "server_1779761946023_30"
},
{
"x": "68.55",
"y": "53.06",
"w": "9.06",
"h": "20.99",
"asset_id": "server_1779761946023_31"
},
{
"x": "78.54",
"y": "53.06",
"w": "9.01",
"h": "20.99",
"asset_id": "server_1779761946023_32"
},
{
"x": "89.36",
"y": "53.22",
"w": "7.45",
"h": "10.11",
"asset_id": "TEMP-03g59cx"
},
{
"x": "89.36",
"y": "64.92",
"w": "7.45",
"h": "9.81",
"asset_id": "TEMP-06l8zjx"
},
{
"x": "48.57",
"y": "77.41",
"w": "9.18",
"h": "21.45",
"asset_id": "server_1779761946023_34"
},
{
"x": "58.56",
"y": "77.41",
"w": "9.23",
"h": "21.45",
"asset_id": "server_1779761946023_35"
},
{
"x": "68.63",
"y": "77.41",
"w": "9.06",
"h": "21.45",
"asset_id": "server_1779761946023_36"
},
{
"x": "78.71",
"y": "77.41",
"w": "8.98",
"h": "21.45",
"asset_id": "server_1779761946023_37"
}
],
"img/location_photo/한맥빌딩/MDF실/MDF_2.png": [
{
"x": "56.59",
"y": "44.53",
"w": "40.65",
"h": "6.90",
"asset_id": "1vbkbzr"
},
{
"x": "56.59",
"y": "54.80",
"w": "40.65",
"h": "6.90",
"asset_id": "0ru63ay"
},
{
"x": "56.59",
"y": "65.94",
"w": "40.65",
"h": "6.90",
"asset_id": "server_1779761946023_40"
}
],
"img/location_photo/한맥빌딩/MDF실/MDF_3.png": [
{
"x": "56.71",
"y": "13.20",
"w": "40.58",
"h": "6.90",
"asset_id": null
},
{
"x": "56.71",
"y": "23.57",
"w": "40.58",
"h": "6.90",
"asset_id": "8aeog58"
},
{
"x": "56.71",
"y": "34.57",
"w": "40.58",
"h": "6.90",
"asset_id": "ywosxiv"
},
{
"x": "56.71",
"y": "44.69",
"w": "40.58",
"h": "6.90",
"asset_id": "1vbkbzr"
},
{
"x": "56.71",
"y": "54.80",
"w": "40.58",
"h": "6.90",
"asset_id": "0ru63ay"
},
{
"x": "56.71",
"y": "65.81",
"w": "40.58",
"h": "6.90",
"asset_id": "server_1779761946023_40"
},
{
"x": "56.71",
"y": "76.05",
"w": "40.58",
"h": "6.90",
"asset_id": null
}
],
"img/location_photo/한맥빌딩/MDF실/MDF_4.png": [
{
"x": "52.36",
"y": "64.02",
"w": "44.60",
"h": "6.73",
"asset_id": "5tbpuy4"
}
],
"img/location_photo/기술개발센터/서버실/서버실_1.png": [
{
"x": "69.45",
"y": "3.30",
"w": "8.58",
"h": "11.45",
"asset_id": "server_1779761946023_41"
},
{
"x": "79.05",
"y": "3.30",
"w": "12.02",
"h": "11.45",
"asset_id": "server_1779761946023_42"
},
{
"x": "90.16",
"y": "26.04",
"w": "8.43",
"h": "21.11",
"asset_id": "server_1779761946023_43"
},
{
"x": "53.04",
"y": "52.91",
"w": "8.43",
"h": "21.11",
"asset_id": "server_1779761946023_44"
},
{
"x": "62.36",
"y": "52.91",
"w": "8.43",
"h": "21.11",
"asset_id": "server_1779761946023_45"
},
{
"x": "71.65",
"y": "52.91",
"w": "8.43",
"h": "21.11",
"asset_id": "server_1779761946023_46"
},
{
"x": "80.87",
"y": "52.91",
"w": "8.43",
"h": "21.11",
"asset_id": "server_1779761946023_47"
},
{
"x": "90.08",
"y": "52.91",
"w": "8.43",
"h": "21.11",
"asset_id": "server_1779761946023_48"
},
{
"x": "43.78",
"y": "78.00",
"w": "8.50",
"h": "21.23",
"asset_id": "19kai41"
},
{
"x": "53.15",
"y": "78.00",
"w": "8.43",
"h": "21.23",
"asset_id": "server_1779761946023_50"
},
{
"x": "62.36",
"y": "78.00",
"w": "8.43",
"h": "21.23",
"asset_id": "server_1779761946023_51"
},
{
"x": "71.36",
"y": "78.00",
"w": "8.43",
"h": "21.23",
"asset_id": "server_1779761946023_52"
},
{
"x": "80.53",
"y": "78.00",
"w": "8.43",
"h": "21.23",
"asset_id": "srlmyar"
},
{
"x": "89.77",
"y": "78.00",
"w": "8.50",
"h": "21.23",
"asset_id": "server_1779761946023_54"
}
],
"img/location_photo/기술개발센터/서버실/서버실_2.png": [
{
"x": "49.60",
"y": "1.93",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_55"
},
{
"x": "49.60",
"y": "12.04",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_56"
},
{
"x": "49.60",
"y": "21.39",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_57"
},
{
"x": "49.60",
"y": "30.73",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_58"
},
{
"x": "49.60",
"y": "39.82",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_59"
},
{
"x": "49.60",
"y": "50.13",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_60"
},
{
"x": "49.60",
"y": "59.28",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_53"
},
{
"x": "49.60",
"y": "68.63",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_62"
},
{
"x": "49.60",
"y": "77.84",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_63"
},
{
"x": "49.60",
"y": "86.93",
"w": "47.19",
"h": "6.75",
"asset_id": "server_1779761946023_64"
}
],
"img/location_photo/TDD_TEST_MAP.png": [
{
"x": "30.50",
"y": "40.25",
"w": "10.00",
"h": "12.00",
"asset_id": null
},
{
"x": "50.00",
"y": "60.00",
"w": "5.00",
"h": "5.00",
"asset_id": null
}
]
}

44
map_editor.html Normal file
View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ITAM Map Coordinate Editor v3.0</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" />
<script src="/qrcode.min.js"></script>
</head>
<body class="editor-body">
<!-- Left: File Selector -->
<div class="file-sidebar" id="file-sidebar">
<!-- Rendered by MapEditor.ts -->
</div>
<!-- Center: Main Editor -->
<div class="editor-container" id="container">
<div class="img-wrapper" id="wrapper">
<img src="" id="target-img" alt="Map Image">
</div>
</div>
<!-- Right: Control Panel -->
<div class="sidebar">
<h2>Map Editor <small class="editor-version">v3.0</small></h2>
<div class="current-path" id="current-path">파일을 선택하세요</div>
<p>
드래그하여 구역을 정의하세요. 저장 버튼을 누르면 즉시 시스템에 반영됩니다.
</p>
<div class="box-list" id="box-list"></div>
<div class="actions" style="display: flex; flex-direction: column; gap: 0.5rem;">
<button id="btn-clear-all" class="btn btn-outline">전체 삭제</button>
<button id="btn-print-map-qrs" class="btn btn-outline btn-primary">이 도면 QR 일괄인쇄</button>
<button id="btn-save-server" class="btn btn-primary">서버에 즉시 저장</button>
<div id="save-status"></div>
</div>
</div>
<script type="module" src="/src/map-editor-main.ts"></script>
</body>
</html>

299
mobile.html Normal file
View File

@@ -0,0 +1,299 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ITAM 모바일 실사 점검</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" />
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<style>
:root {
--bg: #09090b;
--card: #18181b;
--card-border: #27272a;
--primary: #3b82f6;
--primary-hover: #2563eb;
--success: #10b981;
--danger: #ef4444;
--text: #f4f4f5;
--text-muted: #a1a1aa;
--font-family: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: var(--font-family);
}
body {
background-color: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
header {
background-color: var(--card);
border-bottom: 1px solid var(--card-border);
padding: 1rem;
text-align: center;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
header h1 {
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(135deg, #60a5fa, #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--success);
box-shadow: 0 0 8px var(--success);
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
gap: 1rem;
overflow-y: auto;
align-items: center;
justify-content: center;
}
/* Scanner Viewport */
.scanner-container {
width: 100%;
max-width: 400px;
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
border: 2px dashed var(--card-border);
position: relative;
background-color: #000;
}
#reader {
width: 100% !important;
height: 100% !important;
border: none !important;
}
#reader video {
object-fit: cover !important;
width: 100% !important;
height: 100% !important;
}
/* Scan Laser Line Animation */
.scan-laser {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(to right, transparent, var(--primary), transparent);
animation: scan 2s linear infinite;
z-index: 10;
pointer-events: none;
}
@keyframes scan {
0% { top: 0%; }
50% { top: 100%; }
100% { top: 0%; }
}
/* Bottom Info Card */
.info-panel {
width: 100%;
max-width: 400px;
background-color: var(--card);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
flex-shrink: 0;
}
.info-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.info-value {
font-size: 0.95rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-height: 24px;
}
.badge-lock {
background-color: rgba(59, 130, 246, 0.15);
color: var(--primary);
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-size: 0.8rem;
border: 1px solid rgba(59, 130, 246, 0.3);
font-weight: 700;
}
.badge-empty {
color: var(--text-muted);
font-weight: 400;
font-style: italic;
}
.btn-action {
background-color: var(--primary);
color: var(--text);
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-action:hover {
background-color: var(--primary-hover);
}
.btn-action.btn-danger {
background-color: rgba(239, 68, 68, 0.15);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.btn-action.btn-danger:hover {
background-color: rgba(239, 68, 68, 0.25);
}
/* Manual Input Section */
.manual-toggle {
text-align: center;
font-size: 0.8rem;
color: var(--primary);
cursor: pointer;
text-decoration: underline;
}
.manual-form {
display: none;
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
.input-field {
width: 100%;
background-color: var(--bg);
border: 1px solid var(--card-border);
border-radius: 8px;
color: var(--text);
padding: 0.5rem;
font-size: 0.9rem;
}
.input-field:focus {
outline: 1px solid var(--primary);
}
/* Feedbacks Overlay */
.feedback-message {
text-align: center;
padding: 0.5rem;
border-radius: 8px;
font-size: 0.85rem;
display: none;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.feedback-success {
background-color: rgba(16, 185, 129, 0.15);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.feedback-error {
background-color: rgba(239, 68, 68, 0.15);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
</style>
</head>
<body>
<header>
<h1>ITAM 모바일 실사</h1>
<div class="status-dot"></div>
</header>
<main>
<div class="scanner-container">
<div id="reader"></div>
<div class="scan-laser"></div>
</div>
<div class="info-panel">
<!-- 1. 위치 락 정보 -->
<div class="info-section">
<span class="info-label">현재 점검 위치 (Location)</span>
<div class="info-value">
<span id="loc-display" class="badge-empty">위치 QR 코드를 먼저 스캔하세요.</span>
<button id="btn-unlock-loc" class="btn-action btn-danger" style="display: none;">해제</button>
</div>
</div>
<hr style="border: 0; border-top: 1px solid var(--card-border); margin: 0.25rem 0;" />
<!-- 2. 자산 스캔 결과 및 피드백 -->
<div id="scan-feedback" class="feedback-message"></div>
<!-- 3. 수동 입력 토글 및 양식 -->
<div class="info-section">
<span id="btn-toggle-manual" class="manual-toggle">카메라가 안 되나요? 수동 코드로 입력</span>
<div id="manual-form" class="manual-form">
<input type="text" id="manual-code-input" class="input-field" placeholder="위치 또는 자산 코드 입력" />
<button id="btn-submit-manual" class="btn-action w-full">입력 확인</button>
</div>
</div>
</div>
</main>
<script type="module" src="/src/mobile-main.ts"></script>
</body>
</html>

291
package-lock.json generated
View File

@@ -14,9 +14,11 @@
"iconv-lite": "^0.7.2", "iconv-lite": "^0.7.2",
"lucide": "^0.364.0", "lucide": "^0.364.0",
"mysql2": "^3.22.1", "mysql2": "^3.22.1",
"qrcode": "^1.5.4",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/qrcode": "^1.5.6",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.2.0" "vite": "^5.2.0"
} }
@@ -774,11 +776,19 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.19.0" "undici-types": "~7.19.0"
} }
}, },
"node_modules/@types/qrcode": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -801,6 +811,28 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aws-ssl-profiles": { "node_modules/aws-ssl-profiles": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@@ -872,6 +904,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"engines": {
"node": ">=6"
}
},
"node_modules/cfb": { "node_modules/cfb": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
@@ -885,6 +925,16 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/codepage": { "node_modules/codepage": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
@@ -894,6 +944,22 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
@@ -980,6 +1046,14 @@
} }
} }
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -998,6 +1072,11 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.4.2", "version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
@@ -1030,6 +1109,11 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -1187,6 +1271,18 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1247,6 +1343,14 @@
"is-property": "^1.0.2" "is-property": "^1.0.2"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1371,6 +1475,14 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/is-promise": { "node_modules/is-promise": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@@ -1383,6 +1495,17 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/long": { "node_modules/long": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -1575,6 +1698,39 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1584,6 +1740,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"engines": {
"node": ">=8"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.4.2", "version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
@@ -1601,6 +1765,14 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.9", "version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
@@ -1643,6 +1815,22 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.15.1", "version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
@@ -1682,6 +1870,19 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.60.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
@@ -1794,6 +1995,11 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -1918,6 +2124,30 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1959,8 +2189,7 @@
"version": "7.19.2", "version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
@@ -2040,6 +2269,11 @@
} }
} }
}, },
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
},
"node_modules/wmf": { "node_modules/wmf": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
@@ -2058,6 +2292,19 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -2084,6 +2331,44 @@
"engines": { "engines": {
"node": ">=0.8" "node": ">=0.8"
} }
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
} }
} }
} }

View File

@@ -11,6 +11,7 @@
"db-init": "node db_init.js" "db-init": "node db_init.js"
}, },
"devDependencies": { "devDependencies": {
"@types/qrcode": "^1.5.6",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.2.0" "vite": "^5.2.0"
}, },
@@ -21,6 +22,7 @@
"iconv-lite": "^0.7.2", "iconv-lite": "^0.7.2",
"lucide": "^0.364.0", "lucide": "^0.364.0",
"mysql2": "^3.22.1", "mysql2": "^3.22.1",
"qrcode": "^1.5.4",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
} }
} }

View File

@@ -1,134 +1,134 @@
import wmi import wmi
import requests import requests
import json import json
import socket import socket
import platform import platform
import sys import sys
import time import time
def collect_specs(): def collect_specs():
try: try:
c = wmi.WMI() c = wmi.WMI()
computer = c.Win32_ComputerSystem()[0] computer = c.Win32_ComputerSystem()[0]
os_info = c.Win32_OperatingSystem()[0] os_info = c.Win32_OperatingSystem()[0]
proc = c.Win32_Processor()[0] proc = c.Win32_Processor()[0]
board = c.Win32_BaseBoard()[0] board = c.Win32_BaseBoard()[0]
# 1. 상세 GPU 정보 수집 (모든 그래픽 카드) # 1. 상세 GPU 정보 수집 (모든 그래픽 카드)
gpu_list = [] gpu_list = []
for g in c.Win32_VideoController(): for g in c.Win32_VideoController():
gpu_list.append(g.Name) gpu_list.append(g.Name)
gpu_info = ", ".join(gpu_list) if gpu_list else "N/A" gpu_info = ", ".join(gpu_list) if gpu_list else "N/A"
# 2. 모든 저장장치 정보 수집 및 SSD/HDD 구분 # 2. 모든 저장장치 정보 수집 및 SSD/HDD 구분
storage_list = [] storage_list = []
# Windows 8 이상에서 작동하는 상세 저장소 정보 수집 시도 # Windows 8 이상에서 작동하는 상세 저장소 정보 수집 시도
physical_disks = {} physical_disks = {}
try: try:
storage_c = wmi.WMI(namespace="Root\\Microsoft\\Windows\\Storage") storage_c = wmi.WMI(namespace="Root\\Microsoft\\Windows\\Storage")
for d in storage_c.MSFT_PhysicalDisk(): for d in storage_c.MSFT_PhysicalDisk():
# MediaType: 3(HDD), 4(SSD), 0(Unspecified) # MediaType: 3(HDD), 4(SSD), 0(Unspecified)
physical_disks[d.DeviceId] = d.MediaType physical_disks[d.DeviceId] = d.MediaType
except: except:
pass pass
for d in c.Win32_DiskDrive(): for d in c.Win32_DiskDrive():
size_gb = round(float(d.Size) / (1024**3)) if d.Size else 0 size_gb = round(float(d.Size) / (1024**3)) if d.Size else 0
# 미디어 타입 판단 # 미디어 타입 판단
media_type = physical_disks.get(str(d.Index), 0) media_type = physical_disks.get(str(d.Index), 0)
prefix = "" prefix = ""
if media_type == 4: if media_type == 4:
prefix = "[SSD] " prefix = "[SSD] "
elif media_type == 3: elif media_type == 3:
prefix = "[HDD] " prefix = "[HDD] "
else: else:
# 힌트가 없을 경우 모델명으로 추측 # 힌트가 없을 경우 모델명으로 추측
cap = d.Caption.upper() cap = d.Caption.upper()
if "SSD" in cap or "NVME" in cap or "FLASH" in cap: if "SSD" in cap or "NVME" in cap or "FLASH" in cap:
prefix = "[SSD] " prefix = "[SSD] "
else: else:
prefix = "[HDD] " prefix = "[HDD] "
storage_list.append(f"{prefix}{d.Caption} ({size_gb}GB)") storage_list.append(f"{prefix}{d.Caption} ({size_gb}GB)")
# DB 필드(SSD1, SSD2, SSD3)에 나눠 담기 # DB 필드(SSD1, SSD2, SSD3)에 나눠 담기
storage1 = storage_list[0] if len(storage_list) > 0 else "N/A" storage1 = storage_list[0] if len(storage_list) > 0 else "N/A"
storage2 = storage_list[1] if len(storage_list) > 1 else "" storage2 = storage_list[1] if len(storage_list) > 1 else ""
storage3 = storage_list[2] if len(storage_list) > 2 else "" storage3 = storage_list[2] if len(storage_list) > 2 else ""
# 실시간 데이터 추출 # 실시간 데이터 추출
specs = { specs = {
"메인보드": f"{board.Manufacturer} {board.Product}".strip(), "메인보드": f"{board.Manufacturer} {board.Product}".strip(),
"CPU": proc.Name.strip(), "CPU": proc.Name.strip(),
"RAM": f"{round(float(computer.TotalPhysicalMemory) / (1024**3))}GB", "RAM": f"{round(float(computer.TotalPhysicalMemory) / (1024**3))}GB",
"OS": os_info.Caption, "OS": os_info.Caption,
"GPU": gpu_info, "GPU": gpu_info,
"SSD1": storage1, "SSD1": storage1,
"SSD2": storage2, "SSD2": storage2,
"SSD3": storage3, "SSD3": storage3,
"비고": "실시간 에이전트(EXE) 자동 수집" "비고": "실시간 에이전트(EXE) 자동 수집"
} }
return specs return specs
except Exception as e: except Exception as e:
print(f"데이터 수집 중 오류 발생: {e}") print(f"데이터 수집 중 오류 발생: {e}")
return None return None
def send_data(specs, server_url, asset_code): def send_data(specs, server_url, asset_code):
try: try:
# 전송 데이터에 자산코드 추가 (식별용) # 전송 데이터에 자산코드 추가 (식별용)
specs["자산코드"] = asset_code specs["자산코드"] = asset_code
print(f"\n📡 서버로 전송 중... ({server_url})") print(f"\n📡 서버로 전송 중... ({server_url})")
response = requests.post(server_url, json=specs, timeout=10) response = requests.post(server_url, json=specs, timeout=10)
if response.status_code == 200: if response.status_code == 200:
print("✅ 전송 성공! ITAM 시스템에서 확인하세요.") print("✅ 전송 성공! ITAM 시스템에서 확인하세요.")
else: else:
print(f"❌ 전송 실패: 서버 응답 코드 {response.status_code}") print(f"❌ 전송 실패: 서버 응답 코드 {response.status_code}")
except Exception as e: except Exception as e:
print(f"❌ 서버 연결 오류: {e}") print(f"❌ 서버 연결 오류: {e}")
print("서버가 켜져 있는지, URL이 맞는지 확인해주세요.") print("서버가 켜져 있는지, URL이 맞는지 확인해주세요.")
if __name__ == "__main__": if __name__ == "__main__":
print("========================================") print("========================================")
print(" ITAM PC 실시간 사양 수집 에이전트 (v1.1)") print(" ITAM PC 실시간 사양 수집 에이전트 (v1.1)")
print("========================================\n") print("========================================\n")
# 1. 정보 수집 # 1. 정보 수집
print("🔍 하드웨어 정보를 읽어오는 중...") print("🔍 하드웨어 정보를 읽어오는 중...")
data = collect_specs() data = collect_specs()
if data: if data:
print("\n[수집된 실제 사양]") print("\n[수집된 실제 사양]")
display_map = { display_map = {
"메인보드": "메인보드", "메인보드": "메인보드",
"CPU": "CPU", "CPU": "CPU",
"RAM": "RAM", "RAM": "RAM",
"OS": "OS", "OS": "OS",
"GPU": "GPU", "GPU": "GPU",
"SSD1": "Storage 1", "SSD1": "Storage 1",
"SSD2": "Storage 2", "SSD2": "Storage 2",
"SSD3": "Storage 3", "SSD3": "Storage 3",
"비고": "비고" "비고": "비고"
} }
for key, value in data.items(): for key, value in data.items():
if value: # 값이 있는 경우만 표시 if value: # 값이 있는 경우만 표시
label = display_map.get(key, key) label = display_map.get(key, key)
print(f" - {label}: {value}") print(f" - {label}: {value}")
print("\n" + "="*40) print("\n" + "="*40)
asset_code = input("등록할 자산번호를 입력하세요 (예: PC-001): ").strip() asset_code = input("등록할 자산번호를 입력하세요 (예: PC-001): ").strip()
if not asset_code: if not asset_code:
print("❌ 자산번호 없이는 전송할 수 없습니다.") print("❌ 자산번호 없이는 전송할 수 없습니다.")
else: else:
server_ip = input("서버 IP를 입력하세요 (기본값 localhost): ").strip() server_ip = input("서버 IP를 입력하세요 (기본값 localhost): ").strip()
if not server_ip: server_ip = "localhost" if not server_ip: server_ip = "localhost"
target_url = f"http://{server_ip}:3000/api/agent/collect" target_url = f"http://{server_ip}:3000/api/agent/collect"
confirm = input("\n위 정보를 서버로 전송할까요? (y/n): ") confirm = input("\n위 정보를 서버로 전송할까요? (y/n): ")
if confirm.lower() == 'y': if confirm.lower() == 'y':
send_data(data, target_url, asset_code) send_data(data, target_url, asset_code)
print("\n5초 후 프로그램이 종료됩니다...") print("\n5초 후 프로그램이 종료됩니다...")
time.sleep(5) time.sleep(5)

Some files were not shown because too many files have changed in this diff Show More