feat: 부품 마스터 화면 내 직무별 기준 사양 CRUD 및 서브 탭 연동 기능 추가
This commit is contained in:
78
server.js
78
server.js
@@ -28,6 +28,32 @@ const pool = mysql.createPool({
|
|||||||
queueLimit: 0
|
queueLimit: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Database startup check (ensure job_spec_standards table exists)
|
||||||
|
(async () => {
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await pool.getConnection();
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS job_spec_standards (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
job_name VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
cpu_standard VARCHAR(255),
|
||||||
|
ram_standard VARCHAR(100),
|
||||||
|
gpu_standard VARCHAR(100),
|
||||||
|
min_score INT DEFAULT 0,
|
||||||
|
remarks TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`);
|
||||||
|
console.log('✅ job_spec_standards table verification completed.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to verify/create job_spec_standards table:', err);
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Error Handler
|
// Error Handler
|
||||||
const handleError = (res, err, label) => {
|
const handleError = (res, err, label) => {
|
||||||
console.error(`❌ [${label}] Error:`, err);
|
console.error(`❌ [${label}] Error:`, err);
|
||||||
@@ -151,6 +177,7 @@ app.get('/api/assets/master', async (req, res) => {
|
|||||||
const [users] = await connection.query('SELECT * FROM system_users');
|
const [users] = await connection.query('SELECT * FROM system_users');
|
||||||
const [logs] = await connection.query('SELECT * FROM asset_history ORDER BY created_at DESC');
|
const [logs] = await connection.query('SELECT * FROM asset_history ORDER BY created_at DESC');
|
||||||
const [partsMaster] = await connection.query('SELECT * FROM hardware_components_master ORDER BY category, component_name');
|
const [partsMaster] = await connection.query('SELECT * FROM hardware_components_master ORDER BY category, component_name');
|
||||||
|
const [jobSpecs] = await connection.query('SELECT * FROM job_spec_standards ORDER BY job_name');
|
||||||
|
|
||||||
masterData.swInternal = swInternal;
|
masterData.swInternal = swInternal;
|
||||||
masterData.swExternal = swExternal;
|
masterData.swExternal = swExternal;
|
||||||
@@ -158,6 +185,7 @@ app.get('/api/assets/master', async (req, res) => {
|
|||||||
masterData.users = users;
|
masterData.users = users;
|
||||||
masterData.logs = logs;
|
masterData.logs = logs;
|
||||||
masterData.partsMaster = partsMaster;
|
masterData.partsMaster = partsMaster;
|
||||||
|
masterData.jobSpecs = jobSpecs;
|
||||||
|
|
||||||
res.json(masterData);
|
res.json(masterData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -546,6 +574,56 @@ app.delete('/api/hardware-components/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 6.7.1. Get Job Spec Standards
|
||||||
|
app.get('/api/job-specs', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query('SELECT * FROM job_spec_standards ORDER BY job_name');
|
||||||
|
res.json(rows);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(res, err, 'GET JOB SPECS');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6.7.2. Save Job Spec Standard (Add or Update)
|
||||||
|
app.post('/api/job-specs/save', async (req, res) => {
|
||||||
|
const { id, job_name, cpu_standard, ram_standard, gpu_standard, min_score, remarks } = req.body;
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await pool.getConnection();
|
||||||
|
if (id) {
|
||||||
|
await connection.query(
|
||||||
|
'UPDATE job_spec_standards SET job_name = ?, cpu_standard = ?, ram_standard = ?, gpu_standard = ?, min_score = ?, remarks = ? WHERE id = ?',
|
||||||
|
[job_name, cpu_standard, ram_standard, gpu_standard, min_score, remarks, id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await connection.query(
|
||||||
|
'INSERT INTO job_spec_standards (job_name, cpu_standard, ram_standard, gpu_standard, min_score, remarks) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
[job_name, cpu_standard, ram_standard, gpu_standard, min_score, remarks]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
handleError(res, err, 'SAVE JOB SPEC');
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6.7.3. Delete Job Spec Standard
|
||||||
|
app.delete('/api/job-specs/:id', async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await pool.getConnection();
|
||||||
|
await connection.query('DELETE FROM job_spec_standards WHERE id = ?', [id]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
handleError(res, err, 'DELETE JOB SPEC');
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 6.8. Get System Users List
|
// 6.8. Get System Users List
|
||||||
app.get('/api/system-users', async (req, res) => {
|
app.get('/api/system-users', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
176
src/components/Modal/JobSpecModal.ts
Normal file
176
src/components/Modal/JobSpecModal.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { state, saveJobSpec, deleteJobSpec } from '../../core/state';
|
||||||
|
import { BaseModal } from './BaseModal';
|
||||||
|
import { setFieldValue } from './ModalUtils';
|
||||||
|
import { UI_TEXT } from '../../core/schema';
|
||||||
|
|
||||||
|
class JobSpecModal extends BaseModal {
|
||||||
|
constructor() {
|
||||||
|
super('job-spec', '직무별 기준 사양');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderFrameHTML(): string {
|
||||||
|
const sharedStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0;';
|
||||||
|
const inputStyle = sharedStyle;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="job-spec-asset-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content" style="max-width: 500px; width: 100%;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="job-spec-modal-title" style="margin: 0; font-size: 18px; font-weight: 800; color: white;">\${this.title}</h2>
|
||||||
|
<button id="btn-close-job-spec-modal" class="btn-icon" aria-label="닫기" style="font-size: 28px; color: white; background: none; border: none; cursor: pointer; line-height: 1;">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding: 24px; overflow-y: auto;">
|
||||||
|
<form id="job-spec-asset-form" class="grid-form" style="display: flex; flex-direction: column; gap: 16px;">
|
||||||
|
<input type="hidden" id="job-spec-id" name="id" />
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; flex-direction: column; gap: 6px;">
|
||||||
|
<label style="font-size: 11px; font-weight: 700; color: var(--text-muted);">직무명</label>
|
||||||
|
<input type="text" id="job-spec-job-name" name="job_name" placeholder="예: BIM 모델러, 개발자, 엔지니어" required style="\${inputStyle} width: 100%;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; flex-direction: column; gap: 6px;">
|
||||||
|
<label style="font-size: 11px; font-weight: 700; color: var(--text-muted);">권장 CPU 사양</label>
|
||||||
|
<input type="text" id="job-spec-cpu-standard" name="cpu_standard" placeholder="예: Intel Core i7-13700 이상" required style="\${inputStyle} width: 100%;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; flex-direction: column; gap: 6px;">
|
||||||
|
<label style="font-size: 11px; font-weight: 700; color: var(--text-muted);">권장 RAM 사양</label>
|
||||||
|
<input type="text" id="job-spec-ram-standard" name="ram_standard" placeholder="예: 32GB" required style="\${inputStyle} width: 100%;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; flex-direction: column; gap: 6px;">
|
||||||
|
<label style="font-size: 11px; font-weight: 700; color: var(--text-muted);">권장 GPU 사양</label>
|
||||||
|
<input type="text" id="job-spec-gpu-standard" name="gpu_standard" placeholder="예: RTX 4070 이상" required style="\${inputStyle} width: 100%;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; flex-direction: column; gap: 6px;">
|
||||||
|
<label style="font-size: 11px; font-weight: 700; color: var(--text-muted);">성능 기준 점수 (이상)</label>
|
||||||
|
<input type="number" id="job-spec-min-score" name="min_score" placeholder="예: 80" required style="\${inputStyle} width: 100%;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; flex-direction: column; gap: 6px;">
|
||||||
|
<label style="font-size: 11px; font-weight: 700; color: var(--text-muted);">비고 (메모)</label>
|
||||||
|
<textarea id="job-spec-remarks" name="remarks" placeholder="기타 필요 사양 및 안내 사항" style="box-sizing: border-box !important; font-size: 13px; margin: 0; min-height: 80px; width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 4px; resize: vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background: #f8fafc; border-top: 1px solid var(--border-color);">
|
||||||
|
<button id="btn-delete-job-spec-asset" class="btn btn-outline btn-danger" style="height: 42px;">삭제</button>
|
||||||
|
<div class="footer-actions" style="display: flex; gap: 8px;">
|
||||||
|
<button id="btn-revert-job-spec-edit" class="btn btn-outline hidden" style="height: 42px;">수정 취소</button>
|
||||||
|
<button id="btn-cancel-job-spec-modal" class="btn btn-outline" style="height: 42px;">닫기</button>
|
||||||
|
<button id="btn-save-job-spec-asset" class="btn btn-primary" style="height: 42px;">수정</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||||
|
const saveBtn = document.getElementById('btn-save-job-spec-asset')!;
|
||||||
|
const revertBtn = document.getElementById('btn-revert-job-spec-edit')!;
|
||||||
|
const deleteBtn = document.getElementById('btn-delete-job-spec-asset')!;
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', async () => {
|
||||||
|
if (!this.currentAsset) return;
|
||||||
|
if (!this.isEditMode) {
|
||||||
|
this.setEditLockMode('edit');
|
||||||
|
this.isEditMode = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobName = (document.getElementById('job-spec-job-name') as HTMLInputElement).value.trim();
|
||||||
|
const cpuStd = (document.getElementById('job-spec-cpu-standard') as HTMLInputElement).value.trim();
|
||||||
|
const ramStd = (document.getElementById('job-spec-ram-standard') as HTMLInputElement).value.trim();
|
||||||
|
const gpuStd = (document.getElementById('job-spec-gpu-standard') as HTMLInputElement).value.trim();
|
||||||
|
const minScoreStr = (document.getElementById('job-spec-min-score') as HTMLInputElement).value;
|
||||||
|
const remarks = (document.getElementById('job-spec-remarks') as HTMLTextAreaElement).value.trim();
|
||||||
|
|
||||||
|
if (!jobName) {
|
||||||
|
alert('직무명을 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
id: this.currentAsset.id || null,
|
||||||
|
job_name: jobName,
|
||||||
|
cpu_standard: cpuStd,
|
||||||
|
ram_standard: ramStd,
|
||||||
|
gpu_standard: gpuStd,
|
||||||
|
min_score: minScoreStr !== '' ? parseInt(minScoreStr, 10) : 0,
|
||||||
|
remarks: remarks
|
||||||
|
};
|
||||||
|
|
||||||
|
if (await saveJobSpec(updated)) {
|
||||||
|
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||||
|
onSave(); this.close(); closeModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
revertBtn.addEventListener('click', () => {
|
||||||
|
this.setEditLockMode('view');
|
||||||
|
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBtn.addEventListener('click', async () => {
|
||||||
|
if (!this.currentAsset || !this.currentAsset.id) return;
|
||||||
|
if (!confirm('정말로 이 직무별 기준 사양을 삭제하시겠습니까?')) return;
|
||||||
|
|
||||||
|
if (await deleteJobSpec(this.currentAsset.id)) {
|
||||||
|
alert('성공적으로 삭제되었습니다.');
|
||||||
|
onSave(); this.close(); closeModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fillFormData(asset: any): void {
|
||||||
|
setFieldValue('job-spec-id', asset.id || '');
|
||||||
|
setFieldValue('job-spec-job-name', asset.job_name || '');
|
||||||
|
setFieldValue('job-spec-cpu-standard', asset.cpu_standard || '');
|
||||||
|
setFieldValue('job-spec-ram-standard', asset.ram_standard || '');
|
||||||
|
setFieldValue('job-spec-gpu-standard', asset.gpu_standard || '');
|
||||||
|
setFieldValue('job-spec-min-score', asset.min_score !== undefined ? asset.min_score.toString() : '0');
|
||||||
|
setFieldValue('job-spec-remarks', asset.remarks || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onAfterOpen(asset: any, mode: string): void {
|
||||||
|
const titleEl = document.getElementById('job-spec-modal-title');
|
||||||
|
|
||||||
|
if (titleEl) {
|
||||||
|
if (mode === 'add') {
|
||||||
|
titleEl.textContent = '신규 직무별 기준 사양 등록';
|
||||||
|
} else {
|
||||||
|
titleEl.textContent = '직무별 기준 사양 상세 편집';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteBtn = document.getElementById('btn-delete-job-spec-asset')!;
|
||||||
|
const saveBtn = document.getElementById('btn-save-job-spec-asset')!;
|
||||||
|
|
||||||
|
// 추가 모드일 때는 삭제 버튼 숨김
|
||||||
|
deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||||
|
|
||||||
|
if (mode === 'add') {
|
||||||
|
this.setEditLockMode('edit');
|
||||||
|
this.isEditMode = true;
|
||||||
|
saveBtn.textContent = '등록';
|
||||||
|
saveBtn.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
this.setEditLockMode('view');
|
||||||
|
this.isEditMode = false;
|
||||||
|
saveBtn.textContent = '수정';
|
||||||
|
saveBtn.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jobSpecModal = new JobSpecModal();
|
||||||
|
|
||||||
|
export function initJobSpecModal(onSave: () => void, closeModals: () => void) {
|
||||||
|
jobSpecModal.init(onSave, closeModals);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openJobSpecModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
||||||
|
jobSpecModal.open(asset, mode);
|
||||||
|
}
|
||||||
@@ -160,6 +160,16 @@ export const PAGE_DESCRIPTIONS: Record<string, { title: string; description: str
|
|||||||
title: '임직원 사용자 관리',
|
title: '임직원 사용자 관리',
|
||||||
description: 'IT 자산 할당 및 관리의 기준이 되는 사내 임직원(사용자) 정보를 데이터베이스 기반으로 직접 등록하고 수정합니다.',
|
description: 'IT 자산 할당 및 관리의 기준이 되는 사내 임직원(사용자) 정보를 데이터베이스 기반으로 직접 등록하고 수정합니다.',
|
||||||
icon: 'users'
|
icon: 'users'
|
||||||
|
},
|
||||||
|
'부품 마스터': {
|
||||||
|
title: '부품 표준 정보 관리',
|
||||||
|
description: 'PC 사양 적정성 평가의 기준이 되는 부품 표준 정보 및 등급별 감점 점수를 관리합니다.',
|
||||||
|
icon: 'cpu'
|
||||||
|
},
|
||||||
|
'직무별 기준 사양': {
|
||||||
|
title: '직무별 기준 사양 관리',
|
||||||
|
description: 'BIM 모델러, 개발자, 엔지니어 등 사내 직무별 권장 하드웨어 기준 및 성능 합격 점수를 관리합니다.',
|
||||||
|
icon: 'sliders'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface MasterAssetData {
|
|||||||
vip: any[];
|
vip: any[];
|
||||||
mobile?: any[]; // Legacy mobile support
|
mobile?: any[]; // Legacy mobile support
|
||||||
equip?: any[]; // Backward compat
|
equip?: any[]; // Backward compat
|
||||||
|
jobSpecs?: any[];
|
||||||
|
|
||||||
// Backward compatibility
|
// Backward compatibility
|
||||||
subSw: any[];
|
subSw: any[];
|
||||||
@@ -61,7 +62,8 @@ export const state: AppState = {
|
|||||||
cost: [], vip: [],
|
cost: [], vip: [],
|
||||||
subSw: [], permSw: [],
|
subSw: [], permSw: [],
|
||||||
hw: [], sw: [],
|
hw: [], sw: [],
|
||||||
swUsers: [], logs: []
|
swUsers: [], logs: [],
|
||||||
|
jobSpecs: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,6 +81,7 @@ export async function loadMasterDataFromDB() {
|
|||||||
state.masterData = {
|
state.masterData = {
|
||||||
...state.masterData,
|
...state.masterData,
|
||||||
...data,
|
...data,
|
||||||
|
jobSpecs: data.jobSpecs || [],
|
||||||
logs: (data.logs || []).map((l: any) => ({
|
logs: (data.logs || []).map((l: any) => ({
|
||||||
...l,
|
...l,
|
||||||
assetId: l.asset_id || l.assetId,
|
assetId: l.asset_id || l.assetId,
|
||||||
@@ -229,3 +232,38 @@ export async function deleteSystemUser(id: string) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveJobSpec(spec: any) {
|
||||||
|
try {
|
||||||
|
const url = `${API_BASE_URL}/api/job-specs/save`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(spec)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('직무별 기준 사양 저장 실패:', err);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteJobSpec(id: number) {
|
||||||
|
try {
|
||||||
|
const url = `${API_BASE_URL}/api/job-specs/${id}`;
|
||||||
|
const response = await fetch(url, { method: 'DELETE' });
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('직무별 기준 사양 삭제 실패:', err);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import { initSwModal, openSwModal } from './components/Modal/SWModal';
|
|||||||
import { initSwUserModal } from './components/Modal/SWUserModal';
|
import { initSwUserModal } from './components/Modal/SWUserModal';
|
||||||
import { initDomainModal, openDomainModal } from './components/Modal/DomainModal';
|
import { initDomainModal, openDomainModal } from './components/Modal/DomainModal';
|
||||||
import { initPartsMasterModal, openPartsMasterModal } from './components/Modal/PartsMasterModal';
|
import { initPartsMasterModal, openPartsMasterModal } from './components/Modal/PartsMasterModal';
|
||||||
|
import { initJobSpecModal, openJobSpecModal } from './components/Modal/JobSpecModal';
|
||||||
import { initUserModal, openUserModal } from './components/Modal/UserModal';
|
import { initUserModal, openUserModal } from './components/Modal/UserModal';
|
||||||
|
import { activePartsMasterSubTab } from './views/List/PartsMasterListView';
|
||||||
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
|
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
|
||||||
import { initGuide } from './components/Guide';
|
import { initGuide } from './components/Guide';
|
||||||
import { pcFlowModal } from './components/Modal/PCFlowModal';
|
import { pcFlowModal } from './components/Modal/PCFlowModal';
|
||||||
@@ -85,6 +87,7 @@ function initApp() {
|
|||||||
}, closeAllModals);
|
}, closeAllModals);
|
||||||
initDomainModal(() => refreshAllData(), closeAllModals);
|
initDomainModal(() => refreshAllData(), closeAllModals);
|
||||||
initPartsMasterModal(() => refreshAllData(), closeAllModals);
|
initPartsMasterModal(() => refreshAllData(), closeAllModals);
|
||||||
|
initJobSpecModal(() => refreshAllData(), closeAllModals);
|
||||||
initUserModal(() => refreshAllData(), closeAllModals);
|
initUserModal(() => refreshAllData(), closeAllModals);
|
||||||
|
|
||||||
initDashboardDetailModal();
|
initDashboardDetailModal();
|
||||||
@@ -114,7 +117,11 @@ function initApp() {
|
|||||||
|
|
||||||
if (cat === 'hw') {
|
if (cat === 'hw') {
|
||||||
if (tab === '부품 마스터') {
|
if (tab === '부품 마스터') {
|
||||||
openPartsMasterModal({ id: '' } as any, 'add');
|
if (activePartsMasterSubTab === 'job-spec') {
|
||||||
|
openJobSpecModal({ id: '' } as any, 'add');
|
||||||
|
} else {
|
||||||
|
openPartsMasterModal({ id: '' } as any, 'add');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add');
|
openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,171 @@
|
|||||||
import { state } from '../../core/state';
|
import { state } from '../../core/state';
|
||||||
import { openPartsMasterModal } from '../../components/Modal/PartsMasterModal';
|
import { openPartsMasterModal } from '../../components/Modal/PartsMasterModal';
|
||||||
|
import { openJobSpecModal } from '../../components/Modal/JobSpecModal';
|
||||||
import { formatInline } from '../../core/utils';
|
import { formatInline } from '../../core/utils';
|
||||||
import { createListView } from './ListFactory';
|
import { createListView } from './ListFactory';
|
||||||
|
|
||||||
|
export let activePartsMasterSubTab: 'parts-master' | 'job-spec' = 'parts-master';
|
||||||
|
|
||||||
export function renderPartsMasterList(container: HTMLElement) {
|
export function renderPartsMasterList(container: HTMLElement) {
|
||||||
createListView(container, {
|
if (activePartsMasterSubTab === 'parts-master') {
|
||||||
title: '부품 마스터',
|
createListView(container, {
|
||||||
dataSource: () => state.masterData.partsMaster || [],
|
title: '부품 마스터',
|
||||||
searchKeys: ['component_name', 'category', 'score_tier'],
|
dataSource: () => state.masterData.partsMaster || [],
|
||||||
filterOptions: {
|
searchKeys: ['component_name', 'category', 'score_tier'],
|
||||||
keywordLabel: '부품명 / 등급 검색',
|
filterOptions: {
|
||||||
showLoc: false,
|
keywordLabel: '부품명 / 등급 검색',
|
||||||
showDept: false,
|
showLoc: false,
|
||||||
showType: false
|
showDept: false,
|
||||||
},
|
showType: false
|
||||||
onRowClick: (component) => openPartsMasterModal(component, 'view'),
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: 'ID',
|
|
||||||
sortKey: 'id',
|
|
||||||
align: 'center',
|
|
||||||
width: '5%',
|
|
||||||
render: c => c.id.toString()
|
|
||||||
},
|
},
|
||||||
{
|
onRowClick: (component) => openPartsMasterModal(component, 'view'),
|
||||||
header: '분류',
|
columns: [
|
||||||
sortKey: 'category',
|
{
|
||||||
align: 'center',
|
header: 'ID',
|
||||||
width: '15%',
|
sortKey: 'id',
|
||||||
render: c => {
|
align: 'center',
|
||||||
let badgeClass = 'badge-primary';
|
width: '5%',
|
||||||
if (c.category === 'CPU') badgeClass = 'b-primary';
|
render: c => c.id.toString()
|
||||||
else if (c.category === 'GPU') badgeClass = 'b-purple';
|
},
|
||||||
else if (c.category === 'RAM') badgeClass = 'b-green';
|
{
|
||||||
return `<span class="badge ${badgeClass}">${c.category}</span>`;
|
header: '분류',
|
||||||
|
sortKey: 'category',
|
||||||
|
align: 'center',
|
||||||
|
width: '15%',
|
||||||
|
render: c => {
|
||||||
|
let badgeClass = 'badge-primary';
|
||||||
|
if (c.category === 'CPU') badgeClass = 'b-primary';
|
||||||
|
else if (c.category === 'GPU') badgeClass = 'b-purple';
|
||||||
|
else if (c.category === 'RAM') badgeClass = 'b-green';
|
||||||
|
return `<span class="badge ${badgeClass}">${c.category}</span>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '부품 표준 명칭',
|
||||||
|
sortKey: 'component_name',
|
||||||
|
render: c => formatInline(c.component_name || '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '성능 등급',
|
||||||
|
sortKey: 'score_tier',
|
||||||
|
align: 'center',
|
||||||
|
width: '15%',
|
||||||
|
render: c => c.score_tier || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '감점 점수',
|
||||||
|
sortKey: 'deduction',
|
||||||
|
align: 'center',
|
||||||
|
width: '15%',
|
||||||
|
render: c => {
|
||||||
|
const score = c.deduction || 0;
|
||||||
|
let color = '#3b82f6'; // blue
|
||||||
|
if (score >= 20) color = '#ef4444'; // red
|
||||||
|
else if (score >= 10) color = '#f59e0b'; // orange
|
||||||
|
return `<strong style="color: ${color}; font-size: 14px;">-${score}점</strong>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createListView(container, {
|
||||||
|
title: '직무별 기준 사양',
|
||||||
|
dataSource: () => state.masterData.jobSpecs || [],
|
||||||
|
searchKeys: ['job_name', 'cpu_standard', 'ram_standard', 'gpu_standard', 'remarks'],
|
||||||
|
filterOptions: {
|
||||||
|
keywordLabel: '직무명 / 사양 검색',
|
||||||
|
showLoc: false,
|
||||||
|
showDept: false,
|
||||||
|
showType: false
|
||||||
},
|
},
|
||||||
{
|
onRowClick: (jobSpec) => openJobSpecModal(jobSpec, 'view'),
|
||||||
header: '부품 표준 명칭',
|
columns: [
|
||||||
sortKey: 'component_name',
|
{
|
||||||
render: c => formatInline(c.component_name || '-')
|
header: 'ID',
|
||||||
},
|
sortKey: 'id',
|
||||||
{
|
align: 'center',
|
||||||
header: '성능 등급',
|
width: '5%',
|
||||||
sortKey: 'score_tier',
|
render: j => j.id.toString()
|
||||||
align: 'center',
|
},
|
||||||
width: '15%',
|
{
|
||||||
render: c => c.score_tier || '-'
|
header: '직무명',
|
||||||
},
|
sortKey: 'job_name',
|
||||||
{
|
width: '15%',
|
||||||
header: '감점 점수',
|
render: j => `<strong style="color: var(--primary-color); font-size: 14px;">${formatInline(j.job_name || '-')}</strong>`
|
||||||
sortKey: 'deduction',
|
},
|
||||||
align: 'center',
|
{
|
||||||
width: '15%',
|
header: '권장 CPU 사양',
|
||||||
render: c => {
|
sortKey: 'cpu_standard',
|
||||||
const score = c.deduction || 0;
|
render: j => formatInline(j.cpu_standard || '-')
|
||||||
let color = '#3b82f6'; // blue
|
},
|
||||||
if (score >= 20) color = '#ef4444'; // red
|
{
|
||||||
else if (score >= 10) color = '#f59e0b'; // orange
|
header: '권장 RAM 사양',
|
||||||
return `<strong style="color: ${color}; font-size: 14px;">-${score}점</strong>`;
|
sortKey: 'ram_standard',
|
||||||
|
width: '12%',
|
||||||
|
render: j => formatInline(j.ram_standard || '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '권장 GPU 사양',
|
||||||
|
sortKey: 'gpu_standard',
|
||||||
|
render: j => formatInline(j.gpu_standard || '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '기준 점수',
|
||||||
|
sortKey: 'min_score',
|
||||||
|
align: 'center',
|
||||||
|
width: '10%',
|
||||||
|
render: j => `<span style="font-weight: 700;">${j.min_score || 0}점 이상</span>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '비고',
|
||||||
|
sortKey: 'remarks',
|
||||||
|
width: '20%',
|
||||||
|
render: j => formatInline(j.remarks || '-')
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSubTabs(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubTabs(container: HTMLElement) {
|
||||||
|
const header = container.querySelector('.page-header');
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
const tabContainer = document.createElement('div');
|
||||||
|
tabContainer.className = 'sub-tab-container';
|
||||||
|
tabContainer.style.cssText = 'display: flex; gap: 16px; margin-top: 16px; margin-bottom: 16px; border-bottom: 1px solid var(--border-color); padding-bottom: 0;';
|
||||||
|
|
||||||
|
const tab1Active = activePartsMasterSubTab === 'parts-master';
|
||||||
|
const tab2Active = activePartsMasterSubTab === 'job-spec';
|
||||||
|
|
||||||
|
tabContainer.innerHTML = `
|
||||||
|
<button id="tab-parts-master" class="sub-tab-btn ${tab1Active ? 'active' : ''}" style="padding: 10px 16px; border: none; background: none; font-size: 14px; font-weight: 600; cursor: pointer; color: ${tab1Active ? 'var(--primary-color)' : 'var(--text-muted)'}; position: relative; border-bottom: 3px solid ${tab1Active ? 'var(--primary-color)' : 'transparent'};">
|
||||||
|
부품 표준 등급
|
||||||
|
</button>
|
||||||
|
<button id="tab-job-spec" class="sub-tab-btn ${tab2Active ? 'active' : ''}" style="padding: 10px 16px; border: none; background: none; font-size: 14px; font-weight: 600; cursor: pointer; color: ${tab2Active ? 'var(--primary-color)' : 'var(--text-muted)'}; position: relative; border-bottom: 3px solid ${tab2Active ? 'var(--primary-color)' : 'transparent'};">
|
||||||
|
직무별 기준 사양
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
header.parentNode!.insertBefore(tabContainer, header.nextSibling);
|
||||||
|
|
||||||
|
const tabPartsMaster = document.getElementById('tab-parts-master')!;
|
||||||
|
const tabJobSpec = document.getElementById('tab-job-spec')!;
|
||||||
|
|
||||||
|
tabPartsMaster.addEventListener('click', () => {
|
||||||
|
if (activePartsMasterSubTab !== 'parts-master') {
|
||||||
|
activePartsMasterSubTab = 'parts-master';
|
||||||
|
renderPartsMasterList(container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tabJobSpec.addEventListener('click', () => {
|
||||||
|
if (activePartsMasterSubTab !== 'job-spec') {
|
||||||
|
activePartsMasterSubTab = 'job-spec';
|
||||||
|
renderPartsMasterList(container);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user