style: apply Vercel-inspired responsive UI & fluid scaling

This commit is contained in:
2026-06-16 17:43:20 +09:00
parent 155570e8de
commit 73ef13f3a5
21 changed files with 1927 additions and 3068 deletions

View File

@@ -309,21 +309,17 @@ export class PCFlowModal {
private renderUserSuggestions(users: any[], container: HTMLElement, onSelect: (user: any) => void) {
container.innerHTML = '';
if (users.length === 0) {
container.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 13px;">일치하는 사원이 없습니다.</div>';
container.innerHTML = '<div class="autocomplete-item-empty">일치하는 사원이 없습니다.</div>';
container.classList.remove('hidden');
return;
}
users.forEach(u => {
const item = document.createElement('div');
item.style.padding = '8px 12px';
item.style.cursor = 'pointer';
item.style.fontSize = '13px';
item.style.borderBottom = '1px solid #F3F4F6';
item.className = 'suggestion-item';
item.className = 'autocomplete-item';
item.innerHTML = `
<div style="font-weight: 700; color: var(--text-main);">${u.user_name}</div>
<div style="font-size: 11px; color: var(--text-muted); display: flex; gap: 8px;">
<div class="suggestion-name">${u.user_name}</div>
<div class="suggestion-meta">
<span>부서: ${u.dept_name}</span>
<span>|</span>
<span>사번: ${u.emp_no || '-'}</span>
@@ -338,21 +334,17 @@ export class PCFlowModal {
private renderPCSuggestions(pcs: any[], container: HTMLElement, onSelect: (pc: any) => void) {
container.innerHTML = '';
if (pcs.length === 0) {
container.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 13px;">불출 가능한 대기 PC 재고가 없습니다.</div>';
container.innerHTML = '<div class="autocomplete-item-empty">불출 가능한 대기 PC 재고가 없습니다.</div>';
container.classList.remove('hidden');
return;
}
pcs.forEach(p => {
const item = document.createElement('div');
item.style.padding = '8px 12px';
item.style.cursor = 'pointer';
item.style.fontSize = '13px';
item.style.borderBottom = '1px solid #F3F4F6';
item.className = 'suggestion-item';
item.className = 'autocomplete-item';
item.innerHTML = `
<div style="font-weight: 700; color: var(--primary-color);">${p.asset_code} (${p.model_name || '모델명 없음'})</div>
<div style="font-size: 11px; color: var(--text-muted);">
<div class="suggestion-name">${p.asset_code} (${p.model_name || '모델명 없음'})</div>
<div class="suggestion-meta">
사양: CPU ${p.cpu || '-'} / RAM ${p.ram || '-'} / 위치: ${p.location || '-'}
</div>
`;
@@ -433,14 +425,14 @@ export class PCFlowModal {
);
if (userPcs.length === 0) {
userPcsList.innerHTML = '<div style="font-size: 12px; color: var(--text-muted); padding: 8px 0;">이 사용자가 소유한 PC 자산이 없습니다.</div>';
userPcsList.innerHTML = '<div class="empty-list-message">이 사용자가 소유한 PC 자산이 없습니다.</div>';
} else {
userPcsList.innerHTML = userPcs.map(p => {
const isSelected = this.selectedPC && this.selectedPC.id === p.id;
return `
<div class="user-pc-item ${isSelected ? 'selected' : ''}" data-id="${p.id}" style="padding: 10px; border: 1px solid ${isSelected ? 'var(--primary-color)' : 'var(--border-color)'}; border-radius: 4px; cursor: pointer; background: ${isSelected ? 'var(--primary-light)' : 'white'}; transition: all 0.2s;">
<div style="font-weight: 700; font-size: 13px; color: ${isSelected ? 'var(--primary-color)' : 'var(--text-main)'};">${p.asset_code}</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">
<div class="user-pc-item ${isSelected ? 'selected' : ''}" data-id="${p.id}">
<div class="pc-item-code">${p.asset_code}</div>
<div class="pc-item-meta">
${p.model_name || '모델명 없음'} | CPU: ${p.cpu || '-'} | RAM: ${p.ram || '-'}
</div>
</div>
@@ -465,159 +457,134 @@ export class PCFlowModal {
}
private renderHTML(): string {
const overlayStyle = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.4); display: flex; align-items: center; justify-content: center;
z-index: 1000; transition: opacity 0.3s;
`;
const contentStyle = `
background: white; border-radius: 12px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
overflow: hidden; max-height: 90vh; width: 950px; display: flex; flex-direction: column;
`;
const labelStyle = 'display: block; font-size: 13px; font-weight: 700; color: var(--text-muted); margin-bottom: 8px;';
const inputStyle = 'width: 100%; height: 38px; padding: 0 12px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 13px; outline: none; box-sizing: border-box;';
const inputWithIconStyle = 'width: 100%; height: 38px; padding: 0 12px 0 36px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 13px; outline: none; box-sizing: border-box;';
return `
<div id="pc-flow-modal" class="modal-overlay hidden" style="${overlayStyle}">
<div class="modal-content" style="${contentStyle}">
<div id="pc-flow-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header" style="background: var(--primary-color); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color);">
<h2 style="margin: 0; font-size: 18px; font-weight: 800; color: white; display: flex; align-items: center; gap: 8px;">
<div class="modal-header">
<h2 class="modal-title">
<i data-lucide="refresh-cw"></i> PC 이동/반납 (불출/반납/이동)
</h2>
<button id="btn-close-pc-flow-modal" class="btn-icon" aria-label="닫기" style="font-size: 28px; color: white; background: none; border: none; cursor: pointer; line-height: 1;">&times;</button>
<button id="btn-close-pc-flow-modal" class="btn-icon" aria-label="닫기">&times;</button>
</div>
<div class="modal-body" style="padding: 24px; overflow-y: auto; display: flex; gap: 24px;">
<!-- 왼쪽 영역: 입력 폼 -->
<div style="flex: 1.2; display: flex; flex-direction: column; gap: 20px;">
<!-- 1. 처리 유형 -->
<div>
<label style="${labelStyle}">1. 처리 유형 선택</label>
<div style="display: flex; gap: 12px;">
<label class="flow-type-label active" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">
<input type="radio" name="flow-type" value="checkout" checked style="display:none;" />
불출 (지급)
</label>
<label class="flow-type-label" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">
<input type="radio" name="flow-type" value="return" style="display:none;" />
입고 (반납)
</label>
<label class="flow-type-label" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">
<input type="radio" name="flow-type" value="move" style="display:none;" />
이동 (이관)
</label>
<div class="modal-body">
<div class="modal-body-split">
<!-- 왼쪽 영역: 입력 폼 -->
<div class="modal-form-area">
<div class="grid-form flex-col">
<!-- 1. 처리 유형 -->
<div class="form-group">
<label>1. 처리 유형 선택</label>
<div class="view-toggle w-full flex-row">
<label class="flow-type-label toggle-btn active flex-1 text-center">
<input type="radio" name="flow-type" value="checkout" checked class="hidden" />
불출 (지급)
</label>
<label class="flow-type-label toggle-btn flex-1 text-center">
<input type="radio" name="flow-type" value="return" class="hidden" />
입고 (반납)
</label>
<label class="flow-type-label toggle-btn flex-1 text-center">
<input type="radio" name="flow-type" value="move" class="hidden" />
이동 (이관)
</label>
</div>
</div>
<!-- 2. 대상 사용자 검색 -->
<div class="form-group relative">
<label id="user-search-label">2. 대상 사원 검색</label>
<div class="input-with-icon">
<input type="text" id="pc-flow-user-search" placeholder="사원명, 부서, 사번 검색..." />
<i data-lucide="search" class="icon-sm"></i>
</div>
<div id="pc-flow-user-suggestions" class="autocomplete-list hidden"></div>
</div>
<!-- 3. 새 인수자 검색 (이동 시 노출) -->
<div id="target-user-search-container" class="form-group hidden" style="position: relative;">
<label>새 인수 사원 검색</label>
<div class="input-with-icon">
<input type="text" id="pc-flow-target-user-search" placeholder="사원명, 부서, 사번 검색..." />
<i data-lucide="search" class="icon-sm"></i>
</div>
<div id="pc-flow-target-user-suggestions" class="autocomplete-list hidden"></div>
</div>
<!-- 4. 재고 PC 검색 (불출 시 노출) -->
<div id="stock-pc-search-container" class="form-group" style="position: relative;">
<label>3. 불출할 재고 PC 선택</label>
<div class="input-with-icon">
<input type="text" id="pc-flow-stock-search" placeholder="자산코드 또는 모델명 검색..." />
<i data-lucide="monitor" class="icon-sm"></i>
</div>
<div id="pc-flow-stock-suggestions" class="autocomplete-list hidden"></div>
</div>
<!-- 5. 상세 공통 입력 -->
<div class="detail-grid-2col">
<div class="form-group">
<label>처리 일자</label>
<input type="date" id="pc-flow-date" />
</div>
<div class="form-group">
<label>상세 사유</label>
<textarea id="pc-flow-details" rows="2" placeholder="미입력 시 기본 문구로 자동 입력됩니다."></textarea>
</div>
</div>
</div>
</div>
<!-- 2. 대상 사용자 검색 -->
<div style="position: relative;">
<label id="user-search-label" style="${labelStyle}">2. 대상 사원 검색</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="text" id="pc-flow-user-search" placeholder="사원명, 부서, 사번 검색..." style="${inputWithIconStyle}" />
<i data-lucide="search" style="position: absolute; left: 10px; width: 16px; height: 16px; color: var(--text-muted);"></i>
<!-- 오른쪽 영역: 선택 요약 & 사원 소유 자산 목록 -->
<div class="modal-history-area">
<div class="history-header">
<h3>선택 내역 요약</h3>
</div>
<div id="pc-flow-user-suggestions" class="hidden" style="position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; background: white; border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 1000; margin-top: 4px;"></div>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<!-- 사원 요약 카드 -->
<div id="summary-user-card" class="summary-info-card">
<div class="detail-label-sm">대상 사원</div>
<div id="summary-user-name" class="detail-value-lg">선택된 사원 없음</div>
<div id="summary-user-dept" class="detail-label-sm">-</div>
</div>
<!-- 3. 새 인수자 검색 (이동 시 노출) -->
<div id="target-user-search-container" class="hidden" style="position: relative;">
<label style="${labelStyle}">새 인수 사원 검색</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="text" id="pc-flow-target-user-search" placeholder="사원명, 부서, 사번 검색..." style="${inputWithIconStyle}" />
<i data-lucide="search" style="position: absolute; left: 10px; width: 16px; height: 16px; color: var(--text-muted);"></i>
<!-- 인수 사원 요약 카드 (이동 전용) -->
<div id="summary-target-user-card" class="summary-info-card hidden" style="background: var(--primary-light);">
<div class="detail-label-sm">새 인수 사원</div>
<div id="summary-target-user-name" class="detail-value-lg">선택된 사원 없음</div>
<div id="summary-target-user-dept" class="detail-label-sm">-</div>
</div>
<!-- 대상 PC 자산 요약 카드 -->
<div id="summary-pc-card" class="summary-info-card">
<div class="detail-label-sm">대상 PC 자산</div>
<div id="summary-pc-code" class="detail-value-lg" style="color: var(--success);">선택된 PC 없음</div>
<div id="summary-pc-model" class="detail-label-sm">-</div>
</div>
<!-- 사용자 보유 PC 목록 선택 (반납/이동 시) -->
<div id="user-pcs-container" class="form-group hidden">
<label>사원 보유 PC 선택 (클릭하여 매핑)</label>
<div id="user-pcs-list" class="user-pc-selection-list"></div>
</div>
</div>
<div id="pc-flow-target-user-suggestions" class="hidden" style="position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; background: white; border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 1000; margin-top: 4px;"></div>
</div>
<!-- 4. 재고 PC 검색 (불출 시 노출) -->
<div id="stock-pc-search-container" style="position: relative;">
<label style="${labelStyle}">3. 불출할 재고 PC 선택</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="text" id="pc-flow-stock-search" placeholder="자산코드 또는 모델명 검색..." style="${inputWithIconStyle}" />
<i data-lucide="monitor" style="position: absolute; left: 10px; width: 16px; height: 16px; color: var(--text-muted);"></i>
</div>
<div id="pc-flow-stock-suggestions" class="hidden" style="position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; background: white; border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 1000; margin-top: 4px;"></div>
</div>
<!-- 5. 상세 공통 입력 -->
<div style="display: flex; gap: 16px;">
<div style="flex: 1;">
<label style="${labelStyle.replace('margin-bottom: 8px;', 'margin-bottom: 6px;')}">처리 일자</label>
<input type="date" id="pc-flow-date" style="${inputStyle}" />
</div>
<div style="flex: 2;">
<label style="${labelStyle.replace('margin-bottom: 8px;', 'margin-bottom: 6px;')}">상세 사유</label>
<textarea id="pc-flow-details" rows="2" placeholder="미입력 시 기본 문구로 자동 입력됩니다." style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; font-family: inherit; font-size: 13px; resize: none; box-sizing: border-box; outline: none;"></textarea>
</div>
</div>
</div>
<!-- 오른쪽 영역: 선택 요약 & 사원 소유 자산 목록 -->
<div style="flex: 0.8; border-left: 1px solid var(--border-color); padding-left: 24px; display: flex; flex-direction: column; gap: 16px;">
<h3 style="margin: 0; font-size: 14px; font-weight: 800; border-bottom: 1px solid var(--border-color); padding-bottom: 8px;">선택 내역 요약</h3>
<!-- 사원 요약 카드 -->
<div id="summary-user-card" style="padding: 12px; background: var(--bg-light); border: 1px solid var(--border-color); border-radius: 6px; display: flex; flex-direction: column; gap: 4px;">
<div style="font-size: 11px; color: var(--text-muted);">대상 사원</div>
<div id="summary-user-name" style="font-weight: 700; font-size: 14px;">선택된 사원 없음</div>
<div id="summary-user-dept" style="font-size: 12px; color: var(--text-muted);">-</div>
</div>
<!-- 인수 사원 요약 카드 (이동 전용) -->
<div id="summary-target-user-card" class="summary-card hidden" style="padding: 12px; background: #EEF2F6; border: 1px solid var(--border-color); border-radius: 6px; display: flex; flex-direction: column; gap: 4px;">
<div style="font-size: 11px; color: var(--text-muted);">새 인수 사원</div>
<div id="summary-target-user-name" style="font-weight: 700; font-size: 14px;">선택된 사원 없음</div>
<div id="summary-target-user-dept" style="font-size: 12px; color: var(--text-muted);">-</div>
</div>
<!-- 대상 PC 자산 요약 카드 -->
<div id="summary-pc-card" style="padding: 12px; background: var(--bg-light); border: 1px solid var(--border-color); border-radius: 6px; display: flex; flex-direction: column; gap: 4px;">
<div style="font-size: 11px; color: var(--text-muted);">대상 PC 자산</div>
<div id="summary-pc-code" style="font-weight: 700; font-size: 14px; color: var(--primary-color);">선택된 PC 없음</div>
<div id="summary-pc-model" style="font-size: 12px; color: var(--text-muted);">-</div>
</div>
<!-- 사용자 보유 PC 목록 선택 (반납/이동 시) -->
<div id="user-pcs-container" class="hidden" style="display: flex; flex-direction: column; gap: 8px;">
<div style="font-size: 12px; font-weight: 700; color: var(--text-muted);">사원 보유 PC 선택 (클릭하여 매핑)</div>
<div id="user-pcs-list" style="display: flex; flex-direction: column; gap: 8px; max-height: 200px; overflow-y: auto;"></div>
</div>
</div>
</div>
<div class="modal-footer" style="padding: 16px 24px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 12px; background: var(--bg-light);">
<button id="btn-cancel-pc-flow-modal" class="btn btn-outline" style="height: 42px;">취소</button>
<button id="btn-submit-pc-flow" class="btn btn-primary" style="height: 42px;">이동/반납 처리 완료</button>
<div class="modal-footer">
<div></div>
<div class="footer-actions">
<button id="btn-cancel-pc-flow-modal" class="btn btn-outline">취소</button>
<button id="btn-submit-pc-flow" class="btn btn-primary">이동/반납 처리 완료</button>
</div>
</div>
</div>
</div>
<style>
.flow-type-label {
transition: all 0.2s;
border-color: var(--border-color);
background: white;
color: var(--text-muted);
}
.flow-type-label:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
.flow-type-label.active {
border-color: var(--primary-color);
background: var(--primary-light);
color: var(--primary-color);
}
.suggestion-item:hover {
background-color: var(--primary-light) !important;
}
</style>
`;
}
}