5 Commits

70 changed files with 1179 additions and 12 deletions

View File

@@ -9,7 +9,10 @@
- 기존 동작 방식과 성능을 기준(Baseline)으로 삼고, 수정 후에도 **기존의 모든 기능이 무결하게 유지되는지 반드시 테스트하여 입증**한다. - 기존 동작 방식과 성능을 기준(Baseline)으로 삼고, 수정 후에도 **기존의 모든 기능이 무결하게 유지되는지 반드시 테스트하여 입증**한다.
- 검증 결과를 바탕으로 "무엇을, 왜, 어떻게" 바꿀지 상세 보고 후, 사용자로부터 **'진행시켜'** 승인을 얻은 뒤에만 집행한다. - 검증 결과를 바탕으로 "무엇을, 왜, 어떻게" 바꿀지 상세 보고 후, 사용자로부터 **'진행시켜'** 승인을 얻은 뒤에만 집행한다.
4. **선보고 후승인**: 모든 기능 수정 및 코드 변경 전에는 예상 방안을 먼저 보고하고 승인 절차를 거친다. 4. **선보고 후승인**: 모든 기능 수정 및 코드 변경 전에는 예상 방안을 먼저 보고하고 승인 절차를 거친다.
5. **REDGREENRefactor 개발 원칙**: 5. **DB 삭제 및 초기화 절대 엄금 (Strict DB Deletion Policy)**:
- 어떠한 경우에도 `DELETE`, `DROP`, `TRUNCATE` 등 데이터를 삭제하거나 테이블을 초기화하는 작업은 사전에 사용자에게 상세 사유를 보고하고 **명시적 승인**을 얻은 후에만 시행한다.
- 기존 데이터의 가치를 최우선으로 하며, 작업 전 백업 여부를 반드시 확인한다.
6. **REDGREENRefactor 개발 원칙**:
- 모든 기능 개발과 버그 수정은 **RED → GREEN → Refactor** 순서로 진행한다. - 모든 기능 개발과 버그 수정은 **RED → GREEN → Refactor** 순서로 진행한다.
- **RED**: 요구사항을 명확히 표현하는 테스트를 먼저 작성하고, 해당 테스트가 기능 미구현 또는 결함으로 인해 실패하는지 확인한다. - **RED**: 요구사항을 명확히 표현하는 테스트를 먼저 작성하고, 해당 테스트가 기능 미구현 또는 결함으로 인해 실패하는지 확인한다.
- **GREEN**: 실패한 테스트를 통과시키는 데 필요한 최소한의 코드만 구현하며, 불필요한 기능 추가나 구조 변경을 하지 않는다. - **GREEN**: 실패한 테스트를 통과시키는 데 필요한 최소한의 코드만 구현하며, 불필요한 기능 추가나 구조 변경을 하지 않는다.

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.

24
scratch/analyze_codes.cjs Normal file
View File

@@ -0,0 +1,24 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function analyzeCodes() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
// 새 자산들의 연도 분포 확인
const [years] = await connection.query('SELECT DISTINCT purchase_date FROM asset_core WHERE id LIKE "PC_20260615_%"');
console.log('New assets years:', years.map(y => y.purchase_date));
// 기존 자산 코드 패턴 확인
const [existing] = await connection.query('SELECT asset_code FROM asset_core WHERE asset_code LIKE "PC-%" LIMIT 5');
console.log('Existing code sample:', existing);
await connection.end();
}
analyzeCodes().catch(console.error);

View File

@@ -0,0 +1,11 @@
const XLSX = require('xlsx');
const workbook = XLSX.readFile('backupDB_20260602.xlsx');
console.log('Sheet Names:', workbook.SheetNames);
if (workbook.SheetNames.includes('system_users')) {
const sheet = workbook.Sheets['system_users'];
const data = XLSX.utils.sheet_to_json(sheet);
console.log('system_users found! Count:', data.length);
console.log('Sample:', data.slice(0, 2));
} else {
console.log('system_users sheet not found in backupDB_20260602.xlsx');
}

24
scratch/check_codes.cjs Normal file
View File

@@ -0,0 +1,24 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function checkCodes() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log('--- Asset Codes Sample ---');
const [rows] = await connection.query('SELECT id, asset_code, purchase_date FROM asset_core WHERE id LIKE "PC_20260615_%" LIMIT 10');
console.log(rows);
console.log('\n--- Other Asset Codes Sample ---');
const [rows2] = await connection.query('SELECT id, asset_code, purchase_date FROM asset_core WHERE id NOT LIKE "PC_20260615_%" AND asset_code IS NOT NULL LIMIT 5');
console.log(rows2);
await connection.end();
}
checkCodes().catch(console.error);

View File

@@ -0,0 +1,40 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function checkPublicPCs() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log('🔍 공용 PC(Public PC)로 추정되는 자산 조회 중...');
// 사번이 없거나, 사용자명에 '공용'이 포함된 데이터 조회
const [rows] = await connection.query(`
SELECT id, asset_code, user_current, emp_no, current_dept, asset_type
FROM asset_core
WHERE (emp_no IS NULL OR emp_no = '' OR user_current LIKE '%공용%')
AND id LIKE 'PC_20260615_%'
`);
console.log(`📊 발견된 공용 PC 후보: ${rows.length}`);
if (rows.length > 0) {
console.table(rows.slice(0, 20)); // 상위 20개 샘플 출력
// 요약 통계
const summary = {
only_no_emp: rows.filter(r => (!r.emp_no) && !r.user_current.includes('공용')).length,
only_public_name: rows.filter(r => r.emp_no && r.user_current.includes('공용')).length,
both: rows.filter(r => (!r.emp_no) && r.user_current.includes('공용')).length
};
console.log('\n📈 요약 통계:', summary);
}
await connection.end();
}
checkPublicPCs().catch(console.error);

View File

@@ -0,0 +1,77 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function updateAndCompare() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log('🚀 [Step 1 & 2] "undefined" 사번 및 빈 사용자명 정리 중...');
const [updateResult] = await connection.query(`
UPDATE asset_core
SET user_current = '공용', emp_no = NULL
WHERE id LIKE "PC_20260615_%" AND (emp_no = 'undefined' OR emp_no IS NULL OR emp_no = '')
`);
console.log(`✅ 업데이트 완료: ${updateResult.affectedRows}`);
console.log('\n🔍 [Step 3] 엑셀 데이터와 DB asset_type 비교 분석 중...');
const XLSX = require('xlsx');
const workbook = XLSX.readFile('asset_pc (2026.06.15).xlsx');
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const excelData = XLSX.utils.sheet_to_json(sheet);
// DB 데이터 로드
const [dbRows] = await connection.query('SELECT id, asset_type, user_current, emp_no FROM asset_core WHERE id LIKE "PC_20260615_%"');
const dbMap = new Map();
dbRows.forEach(r => dbMap.set(r.id, r));
const mismatches = [];
const publicButExcelPersonal = [];
for (let i = 0; i < excelData.length; i++) {
const excelRow = excelData[i];
const assetId = `PC_20260615_${String(i + 1).padStart(4, '0')}`;
const dbRow = dbMap.get(assetId);
if (!dbRow) continue;
const excelType = excelRow.asset_type || '개인PC';
// 1. 단순 타입 불일치 체크
if (dbRow.asset_type !== excelType) {
mismatches.push({
id: assetId,
excel_type: excelType,
db_type: dbRow.asset_type,
user: dbRow.user_current
});
}
// 2. 엑셀은 '개인PC'인데 데이터는 공용(사번없음)인 경우 탐색
if (excelType === '개인PC' && (!dbRow.emp_no || dbRow.user_current === '공용')) {
publicButExcelPersonal.push({
id: assetId,
excel_user: excelRow.user_current,
excel_dept: excelRow.current_dept,
db_user: dbRow.user_current
});
}
}
console.log(`\n📊 분석 결과:`);
console.log(`- 엑셀과 DB의 asset_type 불일치: ${mismatches.length}`);
console.log(`- 엑셀은 '개인PC'이나 사번이 없어 '공용'으로 잡힌 항목: ${publicButExcelPersonal.length}`);
if (publicButExcelPersonal.length > 0) {
console.log('\n⚠ 엑셀은 개인PC이나 데이터가 미비한 항목 (상위 10개):');
console.table(publicButExcelPersonal.slice(0, 10));
}
await connection.end();
}
updateAndCompare().catch(console.error);

25
scratch/debug_public.cjs Normal file
View File

@@ -0,0 +1,25 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function debugPublic() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
const [rows] = await connection.query(`
SELECT user_current, emp_no, COUNT(*) as count
FROM asset_core
WHERE id LIKE "PC_20260615_%"
GROUP BY user_current, emp_no
HAVING emp_no IS NULL OR emp_no = '' OR user_current LIKE '%공용%' OR user_current = ''
`);
console.table(rows);
await connection.end();
}
debugPublic().catch(console.error);

69
scratch/deep_audit.cjs Normal file
View File

@@ -0,0 +1,69 @@
const XLSX = require('xlsx');
const mysql = require('mysql2/promise');
require('dotenv').config();
async function deepAudit() {
const workbook = XLSX.readFile('asset_pc (2026.06.15).xlsx');
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const excelData = XLSX.utils.sheet_to_json(sheet);
console.log('📊 [Excel Audit] Total Rows:', excelData.length);
// 1. 엑셀 내 asset_type 종류 확인
const excelTypes = new Set();
excelData.forEach(r => excelTypes.add(r.asset_type));
console.log('Excel Asset Types:', Array.from(excelTypes));
// 2. '공용' 키워드가 들어간 모든 행 추출
const publicKeywords = ['공용', '공통', '테스트', 'TEST'];
const potentialPublicInExcel = excelData.filter(r => {
const name = String(r.user_current || '');
const type = String(r.asset_type || '');
const memo = String(r.memo || '');
return publicKeywords.some(k => name.includes(k) || type.includes(k) || memo.includes(k)) || !r.emp_no;
});
console.log(`\n🔍 [Potential Public/Issue Rows in Excel]: ${potentialPublicInExcel.length}`);
console.table(potentialPublicInExcel.slice(0, 30).map(r => ({
emp_no: r.emp_no,
user: r.user_current,
dept: r.current_dept,
type: r.asset_type,
memo: r.memo
})));
// 3. DB와 대조 (특히 엑셀엔 사번이 있는데 DB엔 공용으로 된 게 있는지)
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
const [dbRows] = await connection.query('SELECT id, user_current, emp_no, asset_type FROM asset_core WHERE id LIKE "PC_20260615_%"');
// 엑셀은 개인PC인데 DB는 공용인 경우 (또는 그 반대)
const issues = [];
for (let i = 0; i < excelData.length; i++) {
const ex = excelData[i];
const id = `PC_20260615_${String(i + 1).padStart(4, '0')}`;
const db = dbRows.find(r => r.id === id);
if (!db) continue;
const isExcelPublic = !ex.emp_no || String(ex.user_current).includes('공용');
const isDbPublic = !db.emp_no || String(db.user_current).includes('공용');
if (isExcelPublic !== isDbPublic) {
issues.push({ id, excel_user: ex.user_current, db_user: db.user_current, excel_emp: ex.emp_no, db_emp: db.emp_no });
}
}
console.log(`\n⚠️ [Consistency Issues]: ${issues.length}`);
if (issues.length > 0) console.table(issues);
await connection.end();
}
deepAudit().catch(console.error);

View File

@@ -0,0 +1,61 @@
const XLSX = require('xlsx');
const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.join(__dirname, '../.env') });
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function extractFailures() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🔍 실패 데이터 추출 중...');
const workbook = XLSX.readFile('asset_pc (2026.06.15).xlsx');
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rawData = XLSX.utils.sheet_to_json(sheet);
// 현재 DB에 존재하는 모든 asset_core ID 조회
const [existingRows] = await connection.query('SELECT id FROM asset_core');
const existingIds = new Set(existingRows.map(r => r.id));
const failures = [];
for (let i = 0; i < rawData.length; i++) {
const row = rawData[i];
const assetId = `PC_20260615_${String(i + 1).padStart(4, '0')}`;
// DB에 해당 ID가 없는 경우 = 실패(충돌 등의 이유로 입력되지 않음) 또는 스킵된 데이터
// 하지만 이전 로그에서 'Duplicate entry'로 에러가 났던 항목들을 찾는 것이 목적
// 로직상 ID 생성 규칙에 따라 해당 ID가 DB에 없으면 입력에 실패한 행임
if (!existingIds.has(assetId)) {
failures.push({
excel_row: i + 2,
generated_id: assetId,
...row
});
}
}
if (failures.length > 0) {
const newWb = XLSX.utils.book_new();
const newWs = XLSX.utils.json_to_sheet(failures);
XLSX.utils.book_append_sheet(newWb, newWs, 'Failures');
const fileName = 'asset_pc_failures_20260615.xlsx';
XLSX.writeFile(newWb, fileName);
console.log(`✅ 추출 완료: ${failures.length}건의 실패 데이터를 ${fileName}에 저장했습니다.`);
} else {
console.log('입력되지 않은 데이터가 없습니다.');
}
await connection.end();
}
extractFailures().catch(console.error);

29
scratch/find_public.cjs Normal file
View File

@@ -0,0 +1,29 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function findPotentialPublic() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log('--- Searching for rows with no emp_no or "공용" in user_current ---');
// 사번이 'undefined', 'null', 빈값, 또는 사용자명에 '공용'이 들어간 데이터
const [rows] = await connection.query(`
SELECT id, user_current, emp_no
FROM asset_core
WHERE id LIKE "PC_20260615_%"
AND (emp_no IS NULL OR emp_no = '' OR emp_no = 'undefined' OR user_current LIKE '%공용%')
`);
console.log('Count:', rows.length);
if (rows.length > 0) console.table(rows);
await connection.end();
}
findPotentialPublic().catch(console.error);

View File

@@ -0,0 +1,47 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function fixAssetTypes() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log('🚀 [데이터 정상화] 사번 기준 자산 유형 재설정 시작...');
// 1. 사번이 있는 모든 신규 자산을 '개인PC'로 강제 전환
const [personalResult] = await connection.query(`
UPDATE asset_core
SET asset_type = '개인PC'
WHERE id LIKE "PC_20260615_%"
AND emp_no IS NOT NULL
AND emp_no != ''
`);
console.log(`✅ 개인PC 정상화 완료: ${personalResult.affectedRows}건 (사번 존재 항목)`);
// 2. 사번이 없는 모든 신규 자산을 '공용PC'로 강제 전환
const [publicResult] = await connection.query(`
UPDATE asset_core
SET asset_type = '공용PC', user_current = '공용'
WHERE id LIKE "PC_20260615_%"
AND (emp_no IS NULL OR emp_no = '')
`);
console.log(`✅ 공용PC 정상화 완료: ${publicResult.affectedRows}건 (사번 부재 항목)`);
// 3. 최종 결과 확인
const [rows] = await connection.query(`
SELECT asset_type, COUNT(*) as count
FROM asset_core
WHERE id LIKE "PC_20260615_%"
GROUP BY asset_type
`);
console.log('\n📊 최종 자산 유형 분포:');
console.table(rows);
await connection.end();
}
fixAssetTypes().catch(console.error);

View File

@@ -0,0 +1,122 @@
const XLSX = require('xlsx');
const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.join(__dirname, '../.env') });
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function importAssets() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🚀 [Step 1] 데이터 로드 및 사전 준비...');
// 1. 엑셀 파일 로드
const workbook = XLSX.readFile('asset_pc (2026.06.15).xlsx');
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rawData = XLSX.utils.sheet_to_json(sheet);
// 2. system_users 데이터 맵 생성 (사번 기준 빠른 조회를 위함)
const [userRows] = await connection.query('SELECT emp_no, user_name, dept_name, position, status FROM system_users');
const userMap = new Map();
userRows.forEach(u => userMap.set(String(u.emp_no), u));
// 3. 기존 자산 중복 체크용 맵 생성 (emp_no + asset_type + category)
const [existingAssets] = await connection.query('SELECT emp_no, asset_type, category FROM asset_core');
const existingSet = new Set();
existingAssets.forEach(a => {
existingSet.add(`${a.emp_no}|${a.asset_type}|${a.category}`);
});
console.log(`📊 처리 대상 데이터: ${rawData.length}`);
let skipCount = 0;
let insertCount = 0;
for (let i = 0; i < rawData.length; i++) {
const row = rawData[i];
const empNo = String(row.emp_no);
const assetType = row.asset_type || '개인PC';
const category = row.category || 'PC';
// 중복 체크
if (existingSet.has(`${empNo}|${assetType}|${category}`)) {
skipCount++;
continue;
}
// [Step 2] 데이터 정제
// 1. 사용자 정보 매칭
const matchedUser = userMap.get(empNo);
const userName = matchedUser ? matchedUser.user_name : row.user_current;
const deptName = matchedUser ? matchedUser.dept_name : row.current_dept;
const position = matchedUser ? matchedUser.position : '';
// 2. 날짜 최적화 (purchase_date_1, purchase_date_2 중 최신값)
const d1 = parseInt(row.purchase_date_1) || 0;
const d2 = parseInt(row.purchase_date_2) || 0;
const latestDate = Math.max(d1, d2);
const purchaseDate = latestDate > 0 ? String(latestDate) : '';
// 3. 고유 ID 생성
const assetId = `PC_20260615_${String(i + 1).padStart(4, '0')}`;
const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
try {
// [Step 3] DB 입력
// A. asset_core 입력
await connection.query(
`INSERT INTO asset_core (id, asset_code, category, asset_type, current_role, asset_purpose, service_type,
purchase_corp, purchase_date, memo, manager_primary, current_dept, user_current, emp_no, user_position, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[assetId, assetId, category, assetType, row.current_role, row.asset_purpose, row.service_type,
'', purchaseDate, row.memo || '', '', deptName, userName, empNo, position, now, now]
);
// B. asset_spec 입력
await connection.query(
`INSERT INTO asset_spec (asset_id, model_name, mainboard, cpu, ram, gpu) VALUES (?, ?, ?, ?, ?, ?)`,
[assetId, '', row.mainboard || '', row.cpu || '', row.ram || '', row.gpu || '']
);
// C. asset_volume 입력 (SSD1, SSD2, HDD1~4)
const volumes = [
{ type: 'SSD', cap: row.SDD1, slot: 1 },
{ type: 'SSD', cap: row.SDD2, slot: 2 },
{ type: 'HDD', cap: row.HDD1, slot: 3 },
{ type: 'HDD', cap: row.HDD2, slot: 4 },
{ type: 'HDD', cap: row.HDD3, slot: 5 },
{ type: 'HDD', cap: row.HDD4, slot: 6 }
];
for (const vol of volumes) {
if (vol.cap && vol.cap !== '0' && vol.cap !== 0) {
await connection.query(
`INSERT INTO asset_volume (asset_id, disk_type, capacity, slot_no) VALUES (?, ?, ?, ?)`,
[assetId, vol.type, String(vol.cap), vol.slot]
);
}
}
insertCount++;
existingSet.add(`${empNo}|${assetType}|${category}`); // 실시간 중복 방지 추가
} catch (err) {
console.error(`❌ [${empNo}] 처리 중 오류:`, err.message);
}
}
console.log(`\n✨ 작업 완료!`);
console.log(`- 신규 입력: ${insertCount}`);
console.log(`- 중복 스킵: ${skipCount}`);
await connection.end();
}
importAssets().catch(console.error);

View File

@@ -0,0 +1,164 @@
const XLSX = require('xlsx');
const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.join(__dirname, '../.env') });
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
// 용량 정제 함수
function parseCapacity(val) {
if (!val || val === '0' || val === 0) return null;
let str = String(val).toUpperCase();
// 1. 괄호와 그 안의 내용 제거
str = str.replace(/\(.*\)/g, '').trim();
// 2. 숫자와 단위 분리
const numMatch = str.match(/[\d.]+/);
if (!numMatch) return null;
let num = parseFloat(numMatch[0]);
let unit = 'GB'; // 기본 단위
if (str.includes('TB')) {
unit = 'TB';
} else if (str.includes('GB')) {
// 4자리수 GB인 경우 TB로 전환 (지시사항 1번)
if (num >= 1000) {
num = num / 1000;
unit = 'TB';
} else {
unit = 'GB';
}
} else {
// 단위가 명시되지 않은 경우 숫자의 크기로 판단
if (num >= 1000) {
num = num / 1000;
unit = 'TB';
}
}
return {
capacity: parseFloat(num.toFixed(2)),
unit: unit
};
}
async function importAssets() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🚀 [Step 1] 데이터 로드 및 사전 준비 (정제 로직 강화)...');
const workbook = XLSX.readFile('asset_pc (2026.06.15).xlsx');
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rawData = XLSX.utils.sheet_to_json(sheet);
// system_users 데이터 맵
const [userRows] = await connection.query('SELECT emp_no, user_name, dept_name, position, status FROM system_users');
const userMap = new Map();
userRows.forEach(u => userMap.set(String(u.emp_no), u));
// 기존 자산 중복 체크용 (emp_no + asset_type + category + user_current)
const [existingAssets] = await connection.query('SELECT emp_no, asset_type, category, user_current FROM asset_core');
const existingSet = new Set();
existingAssets.forEach(a => {
existingSet.add(`${a.emp_no || ''}|${a.asset_type}|${a.category}|${a.user_current}`);
});
console.log(`📊 처리 대상 데이터: ${rawData.length}`);
let skipCount = 0;
let insertCount = 0;
let errorCount = 0;
for (let i = 0; i < rawData.length; i++) {
const row = rawData[i];
const empNo = row.emp_no ? String(row.emp_no) : ''; // 사번 없는 행 처리 (지시사항 3번)
const assetType = row.asset_type || '개인PC';
const category = row.category || 'PC';
const userCurrent = row.user_current || '';
// 중복 체크
const dupKey = `${empNo}|${assetType}|${category}|${userCurrent}`;
if (existingSet.has(dupKey)) {
skipCount++;
continue;
}
// [Step 2] 데이터 정제
const matchedUser = empNo ? userMap.get(empNo) : null;
const userName = matchedUser ? matchedUser.user_name : userCurrent;
const deptName = matchedUser ? matchedUser.dept_name : (row.current_dept || '');
const position = matchedUser ? matchedUser.position : '';
const d1 = parseInt(row.purchase_date_1) || 0;
const d2 = parseInt(row.purchase_date_2) || 0;
const purchaseDate = Math.max(d1, d2) > 0 ? String(Math.max(d1, d2)) : '';
const assetId = `PC_20260615_${String(i + 1).padStart(4, '0')}`;
const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
try {
// [Step 3] DB 입력
// A. asset_core
await connection.query(
`INSERT INTO asset_core (id, asset_code, category, asset_type, current_role, asset_purpose, service_type,
purchase_date, memo, current_dept, user_current, emp_no, user_position, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[assetId, assetId, category, assetType, row.current_role || '', row.asset_purpose || '', row.service_type || '',
purchaseDate, row.memo || '', deptName, userName, empNo, position, now, now]
);
// B. asset_spec
await connection.query(
`INSERT INTO asset_spec (asset_id, mainboard, cpu, ram, gpu) VALUES (?, ?, ?, ?, ?)`,
[assetId, row.mainboard || '', row.cpu || '', row.ram || '', row.gpu || '']
);
// C. asset_volume
const volCols = [
{ key: 'SDD1', type: 'SSD', slot: 1 },
{ key: 'SDD2', type: 'SSD', slot: 2 },
{ key: 'HDD1', type: 'HDD', slot: 3 },
{ key: 'HDD2', type: 'HDD', slot: 4 },
{ key: 'HDD3', type: 'HDD', slot: 5 },
{ key: 'HDD4', type: 'HDD', slot: 6 }
];
for (const col of volCols) {
const rawVol = row[col.key];
const parsed = parseCapacity(rawVol);
if (parsed) {
await connection.query(
`INSERT INTO asset_volume (asset_id, disk_type, capacity, unit, slot_no) VALUES (?, ?, ?, ?, ?)`,
[assetId, col.type, parsed.capacity, parsed.unit, col.slot]
);
}
}
insertCount++;
existingSet.add(dupKey);
} catch (err) {
errorCount++;
console.error(`❌ [Row ${i + 2}] ${empNo || 'Public'}: ${err.message}`);
}
}
console.log(`\n✨ 작업 완료!`);
console.log(`- 신규 입력: ${insertCount}`);
console.log(`- 중복 스킵: ${skipCount}`);
console.log(`- 오류 실패: ${errorCount}`);
await connection.end();
}
importAssets().catch(console.error);

View File

@@ -0,0 +1,61 @@
const XLSX = require('xlsx');
const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.join(__dirname, '../.env') });
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function importUsers() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🚀 Excel 데이터 로드 중...');
const workbook = XLSX.readFile('system_User (20260615).xlsx');
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(sheet);
console.log(`📊 총 ${data.length}개의 데이터를 찾았습니다.`);
// 기존 데이터 삭제 여부 (사용자 요구사항에 따라 결정 가능하지만, 보통 초기화 후 재입입)
// 여기서는 중복 방지를 위해 기존 데이터를 삭제하고 새로 넣는 방식을 취하겠습니다.
console.log('🧹 기존 system_users 데이터 삭제 중...');
await connection.query('DELETE FROM system_users');
console.log('📥 데이터 삽입 중...');
let successCount = 0;
for (let i = 0; i < data.length; i++) {
const row = data[i];
const { emp_no, user_name, dept_name, position, status } = row;
// ID 생성 (USR_ + 인덱스 001 형식)
const id = `USR_${String(i + 1).padStart(3, '0')}`;
const createdAt = new Date().toISOString().replace('T', ' ').substring(0, 19);
try {
await connection.query(
'INSERT INTO system_users (id, emp_no, user_name, dept_name, position, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[id, String(emp_no), user_name, dept_name, position, status, createdAt]
);
successCount++;
} catch (err) {
console.error(`❌ 삽입 실패 (Row ${i + 2}):`, err.message);
}
}
console.log(`✅ 완료: ${successCount}개의 사용자가 성공적으로 등록되었습니다.`);
await connection.end();
}
importUsers().catch(err => {
console.error('❌ 작업 중 오류 발생:', err);
process.exit(1);
});

View File

@@ -0,0 +1,7 @@
const XLSX = require('xlsx');
const workbook = XLSX.readFile('asset_pc (2026.06.15).xlsx');
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });
console.log('Headers:', JSON.stringify(data[0], null, 2));
console.log('Sample Row 1:', JSON.stringify(data[1], null, 2));

6
scratch/peek_excel.cjs Normal file
View File

@@ -0,0 +1,6 @@
const XLSX = require('xlsx');
const workbook = XLSX.readFile('system_User (20260615).xlsx');
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });
console.log(JSON.stringify(data.slice(0, 5), null, 2));

18
scratch/raw_check.cjs Normal file
View File

@@ -0,0 +1,18 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function rawCheck() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
const [rows] = await connection.query('SELECT user_current, emp_no FROM asset_core WHERE id LIKE "PC_20260615_%" LIMIT 10');
console.log(rows);
await connection.end();
}
rawCheck().catch(console.error);

View File

@@ -0,0 +1,85 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function rebuildAssetCodes() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log('🚀 [Step 1] 신규 자산 구매일 업데이트 (YYYY-12-01)...');
// 1. 오늘 입력한 자산들 조회
const [rows] = await connection.query(
'SELECT id, purchase_date FROM asset_core WHERE id LIKE "PC_20260615_%"'
);
console.log(`대상 자산: ${rows.length}`);
// 2. 구매일자 업데이트 (연도만 있는 경우 -12-01 추가)
for (const row of rows) {
if (row.purchase_date && row.purchase_date.length === 4) {
const newDate = `${row.purchase_date}-12-01`;
await connection.query(
'UPDATE asset_core SET purchase_date = ? WHERE id = ?',
[newDate, row.id]
);
}
}
console.log('✅ 구매일 업데이트 완료.');
console.log('\n🚀 [Step 2] 자산번호(asset_code) 재매핑 시작...');
// 3. 연도별로 그룹화하여 자산번호 부여
// 연도 목록 추출
const [yearRows] = await connection.query(
'SELECT DISTINCT LEFT(purchase_date, 4) as year FROM asset_core WHERE id LIKE "PC_20260615_%" ORDER BY year'
);
for (const yRow of yearRows) {
const year = yRow.year;
const yearMonth = `${year}12`;
const pattern = `PC-${yearMonth}-%`;
console.log(`--- [${year}년] 처리 중 ---`);
// 해당 연도/월의 기존 최대 순번 조회
const [maxRows] = await connection.query(
'SELECT asset_code FROM asset_core WHERE asset_code LIKE ? AND id NOT LIKE "PC_20260615_%"',
[pattern]
);
let maxSeq = 0;
maxRows.forEach(r => {
const parts = r.asset_code.split('-');
const seq = parseInt(parts[2]);
if (seq > maxSeq) maxSeq = seq;
});
console.log(`기존 최대 순번: ${maxSeq}`);
// 해당 연도 자산들 순차적으로 번호 부여
const [assetsOfYear] = await connection.query(
'SELECT id FROM asset_core WHERE id LIKE "PC_20260615_%" AND purchase_date LIKE ? ORDER BY id',
[`${year}-12%`]
);
let currentSeq = maxSeq + 1;
for (const asset of assetsOfYear) {
const newCode = `PC-${yearMonth}-${String(currentSeq).padStart(4, '0')}`;
await connection.query(
'UPDATE asset_core SET asset_code = ? WHERE id = ?',
[newCode, asset.id]
);
currentSeq++;
}
console.log(`신규 부여 완료: ${assetsOfYear.length}건 (순번 ${maxSeq + 1} ~ ${currentSeq - 1})`);
}
console.log('\n✨ 모든 작업이 완료되었습니다.');
await connection.end();
}
rebuildAssetCodes().catch(console.error);

View File

@@ -0,0 +1,85 @@
const XLSX = require('xlsx');
const mysql = require('mysql2/promise');
require('dotenv').config();
async function reexamineData() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log('🧐 [전수 조사] 엑셀 vs DB 데이터 비교 분석...');
// 1. 엑셀 데이터 로드
const workbook = XLSX.readFile('asset_pc (2026.06.15).xlsx');
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const excelRows = XLSX.utils.sheet_to_json(sheet);
// 2. DB 데이터 로드
const [dbRows] = await connection.query(`
SELECT id, asset_code, asset_type, user_current, emp_no, current_dept
FROM asset_core
WHERE id LIKE "PC_20260615_%"
`);
const dbMap = new Map();
dbRows.forEach(r => dbMap.set(r.id, r));
const report = {
total: excelRows.length,
publicInExcelWithEmpNo: [], // 엑셀은 공용PC인데 사번이 있는 경우
personalInExcelNoEmpNo: [], // 엑셀은 개인PC인데 사번이 없는 경우
typeMismatch: [], // 엑셀과 DB의 asset_type이 다른 경우
userMismatch: [] // 사용자명이 크게 다른 경우
};
for (let i = 0; i < excelRows.length; i++) {
const ex = excelRows[i];
const id = `PC_20260615_${String(i + 1).padStart(4, '0')}`;
const db = dbMap.get(id);
if (!db) continue;
const exType = ex.asset_type || '개인PC';
const exEmpNo = ex.emp_no ? String(ex.emp_no) : null;
const exUser = ex.user_current || '';
// A. 공용PC인데 사번이 있는 경우 (가장 큰 혼란 포인트)
if (exType === '공용PC' && exEmpNo) {
report.publicInExcelWithEmpNo.push({ id, exUser, exEmpNo, exDept: ex.current_dept });
}
// B. 개인PC인데 사번이 없는 경우
if (exType === '개인PC' && !exEmpNo) {
report.personalInExcelNoEmpNo.push({ id, exUser, exDept: ex.current_dept });
}
// C. DB와의 타입 불일치 (현재 DB 상태 체크)
if (db.asset_type !== exType) {
report.typeMismatch.push({ id, exType, dbType: db.asset_type, user: db.user_current });
}
}
console.log('\n================================================');
console.log(`📊 전수 조사 요약 (총 ${report.total}건)`);
console.log(`1. 엑셀은 '공용PC'이나 '사번'이 있는 항목: ${report.publicInExcelWithEmpNo.length}`);
console.log(`2. 엑셀은 '개인PC'이나 '사번'이 없는 항목: ${report.personalInExcelNoEmpNo.length}`);
console.log(`3. 현재 DB와 엑셀의 '자산유형' 불일치: ${report.typeMismatch.length}`);
console.log('================================================\n');
if (report.publicInExcelWithEmpNo.length > 0) {
console.log('⚠️ [그룹 1] 공용PC인데 실사용자/관리자가 지정된 사례 (샘플 15건):');
console.table(report.publicInExcelWithEmpNo.slice(0, 15));
}
if (report.personalInExcelNoEmpNo.length > 0) {
console.log('\n⚠ [그룹 2] 개인PC인데 사번 정보가 누락된 사례 (샘플 15건):');
console.table(report.personalInExcelNoEmpNo.slice(0, 15));
}
await connection.end();
}
reexamineData().catch(console.error);

View File

@@ -0,0 +1,92 @@
const XLSX = require('xlsx');
const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.join(__dirname, '../.env') });
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function restoreAndMerge() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🔄 데이터 복구 및 병합 시작...');
// 1. 백업 파일에서 기존 데이터(212건) 로드
const workbookBackup = XLSX.readFile('backupDB_20260602.xlsx');
const oldUsers = XLSX.utils.sheet_to_json(workbookBackup.Sheets['system_users']);
// 2. 신규 파일에서 데이터(987건) 로드
const workbookNew = XLSX.readFile('system_User (20260615).xlsx');
const newUsers = XLSX.utils.sheet_to_json(workbookNew.Sheets[workbookNew.SheetNames[0]]);
console.log(`기본 백업 데이터: ${oldUsers.length}`);
console.log(`신규 추가 데이터: ${newUsers.length}`);
// 테이블 비우기 (실수를 바로잡기 위해 다시 시작)
await connection.query('DELETE FROM system_users');
const insertedEmpNos = new Set();
let restoreCount = 0;
let addCount = 0;
// 3. 기존 데이터 복구 (ID 보존 시도)
for (const user of oldUsers) {
const { id, emp_no, user_name, dept_name, position, status, created_at } = user;
// 엑셀 날짜 처리 (숫자로 되어 있을 경우)
let finalCreatedAt = created_at;
if (typeof created_at === 'number') {
const date = new Date((created_at - 25569) * 86400 * 1000);
finalCreatedAt = date.toISOString().replace('T', ' ').substring(0, 19);
}
try {
await connection.query(
'INSERT INTO system_users (id, emp_no, user_name, dept_name, position, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[id, String(emp_no), user_name, dept_name, position, status, finalCreatedAt]
);
insertedEmpNos.add(String(emp_no));
restoreCount++;
} catch (err) {
console.error(`❌ 복구 실패 (emp_no: ${emp_no}):`, err.message);
}
}
// 4. 신규 데이터 추가 (중복 제외)
for (let i = 0; i < newUsers.length; i++) {
const user = newUsers[i];
const { emp_no, user_name, dept_name, position, status } = user;
const strEmpNo = String(emp_no);
if (insertedEmpNos.has(strEmpNo)) {
continue; // 이미 복구된 데이터는 스킵
}
// 신규 데이터용 ID 생성 (기존 ID와 겹치지 않게 'NEW_' 접두어 또는 시퀀스 사용)
// 여기서는 단순히 시퀀스로 처리 (최대 ID 확인 후 +1 하는 방식이 좋으나 여기선 간단히)
const id = `USR_N_${String(i + 1).padStart(4, '0')}`;
const createdAt = new Date().toISOString().replace('T', ' ').substring(0, 19);
try {
await connection.query(
'INSERT INTO system_users (id, emp_no, user_name, dept_name, position, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[id, strEmpNo, user_name, dept_name, position, status, createdAt]
);
addCount++;
} catch (err) {
console.error(`❌ 추가 실패 (emp_no: ${emp_no}):`, err.message);
}
}
console.log(`✅ 복구 완료: 기존 ${restoreCount}건 복구, 신규 ${addCount}건 추가 (총 ${restoreCount + addCount}건)`);
await connection.end();
}
restoreAndMerge().catch(console.error);

View File

@@ -0,0 +1,32 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function updateDepartments() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306')
});
console.log("🚀 부서명 '삼안' 통합 업데이트 시작...");
const [result] = await connection.query(`
UPDATE asset_core
SET current_dept = '삼안'
WHERE current_dept NOT IN ('총괄기획실', '기술개발센터', '현타', '장헌', '한맥', 'PTC', '', '삼안')
AND current_dept IS NOT NULL
`);
console.log(`✅ 업데이트 완료: ${result.affectedRows}건의 부서명이 '삼안'으로 변경되었습니다.`);
// 최종 확인용 카운트
const [rows] = await connection.query('SELECT current_dept, COUNT(*) as count FROM asset_core GROUP BY current_dept');
console.log('\n📊 최종 부서 분포:');
console.table(rows);
await connection.end();
}
updateDepartments().catch(console.error);

View File

@@ -1,6 +1,6 @@
import { state, saveAsset, deleteAsset } from '../../core/state'; import { state, saveAsset, deleteAsset } from '../../core/state';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { calculatePcScoreDeductive, getPcGrade } from '../../core/utils'; import { calculatePcScoreDeductive, getPcGrade, API_BASE_URL } from '../../core/utils';
import { import {
generateOptionsHTML, generateOptionsHTML,
setFieldValue, setFieldValue,
@@ -377,6 +377,30 @@ class HwAssetModal extends BaseModal {
return; return;
} }
// 자산코드가 비어있는 경우 자동 생성 처리
let assetCode = getFieldValue('hw-asset_code').trim();
if (!assetCode) {
const cat = categorySelect.value;
if (!cat) { alert('구분을 먼저 선택해주세요.'); return; }
const prefix = TYPE_PREFIX_MAP[cat] || 'ETC';
const purchaseDate = (document.getElementById('hw-purchase_date') as HTMLInputElement)?.value || '';
try {
const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
const data = await res.json();
if (data.nextCode) {
setFieldValue('hw-asset_code', data.nextCode);
assetCode = data.nextCode;
} else {
alert('자산코드 자동 생성에 실패했습니다. 수동으로 생성 버튼을 눌러주세요.');
return;
}
} catch (err) {
console.error('코드 자동 생성 실패:', err);
alert('자산코드 자동 생성 중 오류가 발생했습니다.');
return;
}
}
// 동적 볼륨 데이터 수집 // 동적 볼륨 데이터 수집
const vols: any[] = []; const vols: any[] = [];
document.querySelectorAll('#hw-volume-container .volume-row').forEach((row, idx) => { document.querySelectorAll('#hw-volume-container .volume-row').forEach((row, idx) => {
@@ -704,7 +728,7 @@ class HwAssetModal extends BaseModal {
private async fetchMapConfig() { private async fetchMapConfig() {
try { try {
const res = await fetch(`http://${location.hostname}:3000/api/maps`); const res = await fetch(`${API_BASE_URL}/api/maps`);
this.dynamicMapConfig = await res.json(); this.dynamicMapConfig = await res.json();
} catch (err) { console.error('Failed to fetch map config:', err); } } catch (err) { console.error('Failed to fetch map config:', err); }
} }
@@ -901,7 +925,7 @@ class HwAssetModal extends BaseModal {
private async fetchMasterComponents(): Promise<void> { private async fetchMasterComponents(): Promise<void> {
try { try {
const res = await fetch(`http://${location.hostname}:3000/api/hardware-components`); const res = await fetch(`${API_BASE_URL}/api/hardware-components`);
this.masterComponents = await res.json(); this.masterComponents = await res.json();
} catch (err) { console.error('Failed to fetch master components:', err); } } catch (err) { console.error('Failed to fetch master components:', err); }
} }

View File

@@ -125,10 +125,13 @@ export function setEditLock(
const inputs = form.querySelectorAll('input, select, textarea'); const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => { inputs.forEach(input => {
const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
// 자산번호 및 ID 필드는 편집 모드에서도 잠금 유지 // 자산번호 및 ID 필드는 편집 모드에서도 잠금 유지 (disabled는 해제하되 readOnly를 적용하여 폼 데이터 수집 가능하게 함)
if (el.name !== 'asset_code' && !el.id.includes('asset-id') && !el.id.includes('id-hidden')) { if (el.name !== 'asset_code' && !el.id.includes('asset-id') && !el.id.includes('id-hidden')) {
el.disabled = false; el.disabled = false;
if ('readOnly' in el) (el as HTMLInputElement).readOnly = false; if ('readOnly' in el) (el as HTMLInputElement).readOnly = false;
} else {
el.disabled = false;
if ('readOnly' in el) (el as HTMLInputElement).readOnly = true;
} }
}); });

View File

@@ -537,6 +537,8 @@
background-color: var(--canvas-soft-2); background-color: var(--canvas-soft-2);
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
max-width: 100%;
max-height: 100%;
} }
.layout-map-container.readonly { .layout-map-container.readonly {
@@ -546,12 +548,15 @@
.image-marker-wrapper { .image-marker-wrapper {
position: relative; position: relative;
display: inline-block; display: inline-block;
max-width: 100%;
max-height: 100%;
} }
.layout-map-img { .layout-map-img {
display: block; display: block;
max-width: 100%; max-width: 100%;
max-height: 75vh; max-height: 70vh;
object-fit: contain;
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;
} }

View File

@@ -25,10 +25,6 @@ export async function renderLocationView(container: HTMLElement) {
: []; : [];
const mapPath = locImages[currentPage] || ''; const mapPath = locImages[currentPage] || '';
// 조회 모드: 설정 파일에 정의된 asset_id를 기준으로 자산 데이터 매핑
const allBoxes = mapConfig[mapPath] || [];
const boxes = allBoxes.filter((box: any) => box.asset_id != null);
// 모든 하드웨어 카테고리에서 자산 검색 // 모든 하드웨어 카테고리에서 자산 검색
const allHwAssets = [ const allHwAssets = [
...state.masterData.pc, ...state.masterData.pc,
@@ -41,6 +37,50 @@ export async function renderLocationView(container: HTMLElement) {
...state.masterData.pcParts ...state.masterData.pcParts
]; ];
// map_config.json에 설정된 모든 박스를 복사해서 작업용으로 사용
const tempBoxes = (mapConfig[mapPath] || []).map((b: any) => ({ ...b }));
// DB 데이터에서 현재 지도(mapPath) 및 위치와 좌표 정보(loc_x, loc_y)가 일치하는 자산 추출
allHwAssets.forEach((asset: any) => {
const photoPath = asset.location_photo || asset.loc_img || '';
const hasCoords = asset.loc_x != null && asset.loc_y != null && asset.loc_x !== '' && asset.loc_y !== '' && asset.loc_x !== 'null' && asset.loc_y !== 'null';
if (hasCoords && photoPath.trim() === mapPath.trim()) {
const ax = parseFloat(asset.loc_x);
const ay = parseFloat(asset.loc_y);
// map_config.json에서 읽어온 박스들 중 x, y 좌표가 일치하는 빈 박스가 있는지 찾음 (오차범위 0.1 고려)
const matchedBox = tempBoxes.find((b: any) => {
const bx = parseFloat(b.x);
const by = parseFloat(b.y);
return Math.abs(bx - ax) < 0.1 && Math.abs(by - ay) < 0.1;
});
if (matchedBox) {
// 이미 매칭된 박스가 존재하고 asset_id가 비어있다면 해당 박스에 asset_id를 주입
if (matchedBox.asset_id == null) {
matchedBox.asset_id = asset.id;
}
} else {
// 일치하는 기존 박스가 없을 때만 4x4 크기의 임시 박스로 동적 생성
const alreadyMatched = tempBoxes.some((b: any) => b.asset_id === asset.id);
if (!alreadyMatched) {
tempBoxes.push({
asset_id: asset.id,
x: asset.loc_x,
y: asset.loc_y,
w: '4',
h: '4',
name: asset.asset_purpose || asset.asset_code || '미지정 자산'
});
}
}
}
});
// 최종적으로 asset_id가 null이 아닌(자산이 정상 매핑되거나 갱신된) 박스들만 남겨서 렌더링
const boxes = tempBoxes.filter((b: any) => b.asset_id != null);
container.innerHTML = ` container.innerHTML = `
<div class="location-view-wrapper"> <div class="location-view-wrapper">
<!-- 상단 통합 바 (Unified Search Bar) --> <!-- 상단 통합 바 (Unified Search Bar) -->

BIN
system_User (20260615).xlsx Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,17 +1,19 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { resolve } from 'path'; import { resolve } from 'path';
const proxyTarget = process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:3000';
export default defineConfig({ export default defineConfig({
server: { server: {
port: 8080, port: 8080,
host: true, // Listen on all local IPs host: true, // Listen on all local IPs
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: proxyTarget,
changeOrigin: true, changeOrigin: true,
}, },
'/uploads': { '/uploads': {
target: 'http://localhost:3000', target: proxyTarget,
changeOrigin: true, changeOrigin: true,
} }
} }