refactor: promote 8081 design system and served app structure

This commit is contained in:
hyunho
2026-04-01 14:50:08 +09:00
parent 637b390024
commit d0e055973e
50 changed files with 24157 additions and 820 deletions

View File

@@ -1,38 +1,41 @@
@import url("/design-tokens.css?v=20260401-01");
@import url("/design-patterns.css?v=20260401-01");
:root {
--font-sans: "Pretendard", sans-serif;
--font-sans: var(--ds-font-sans);
--color-bg: #f1f5f9;
--color-bg-soft: #eef2ff;
--color-surface: #ffffff;
--color-surface-soft: rgba(255, 255, 255, 0.88);
--color-surface-strong: #e2e8f0;
--color-text: #1e293b;
--color-text-soft: #475569;
--color-text-muted: #64748b;
--color-border: #cbd5e1;
--color-border-soft: rgba(148, 163, 184, 0.3);
--color-header: #1e293b;
--color-header-soft: #334155;
--color-accent: #4f46e5;
--color-accent-soft: #e0e7ff;
--color-accent-strong: #4338ca;
--color-bg: var(--ds-bg);
--color-bg-soft: var(--ds-bg-soft);
--color-surface: var(--ds-panel);
--color-surface-soft: var(--ds-panel-soft);
--color-surface-strong: var(--ds-panel-strong);
--color-text: var(--ds-ink);
--color-text-soft: var(--ds-text-soft);
--color-text-muted: var(--ds-text-muted);
--color-border: var(--ds-line);
--color-border-soft: var(--ds-line-soft);
--color-header: var(--ds-brand);
--color-header-soft: var(--ds-brand-soft);
--color-accent: var(--ds-accent);
--color-accent-soft: var(--ds-accent-soft);
--color-accent-strong: var(--ds-accent-strong);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 18px;
--radius-xl: 24px;
--radius-pill: 999px;
--radius-sm: var(--ds-radius-sm);
--radius-md: var(--ds-radius-md);
--radius-lg: var(--ds-radius-lg);
--radius-xl: var(--ds-radius-xl);
--radius-pill: var(--ds-radius-pill);
--shadow-soft: 0 4px 14px rgba(15, 23, 42, 0.08);
--shadow-card: 0 18px 44px rgba(15, 23, 42, 0.12);
--shadow-float: 0 18px 36px rgba(79, 70, 229, 0.16);
--shadow-soft: var(--ds-shadow-soft);
--shadow-card: var(--ds-shadow-card);
--shadow-float: var(--ds-shadow-float);
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-1: var(--ds-space-1);
--space-2: var(--ds-space-2);
--space-3: var(--ds-space-3);
--space-4: var(--ds-space-4);
--space-5: var(--ds-space-5);
--space-6: var(--ds-space-6);
}
* {
@@ -46,15 +49,13 @@ body {
min-height: 100%;
font-family: var(--font-sans);
color: var(--color-text);
background:
radial-gradient(circle at top left, rgba(79, 70, 229, 0.12), transparent 22%),
radial-gradient(circle at bottom right, rgba(148, 163, 184, 0.18), transparent 28%),
var(--color-bg);
background: var(--ds-bg-gradient);
}
body {
min-height: 100vh;
overflow: hidden;
background: var(--ds-bg-gradient);
}
button,
@@ -92,18 +93,18 @@ a {
.ui-button-secondary {
border: 1px solid var(--color-border-soft);
color: var(--color-text);
background: rgba(255, 255, 255, 0.72);
background: var(--ds-surface-tint);
}
.ui-input {
border: 1px solid var(--color-border-soft);
border-radius: var(--radius-pill);
background: rgba(255, 255, 255, 0.88);
background: var(--ds-surface-tint-strong);
color: var(--color-text);
outline: none;
}
.ui-input:focus {
border-color: rgba(79, 70, 229, 0.45);
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.08);
border-color: rgba(47, 153, 115, 0.45);
box-shadow: 0 0 0 4px rgba(47, 153, 115, 0.1);
}

File diff suppressed because it is too large Load Diff

View File

@@ -860,18 +860,20 @@ function openUnitAddModal(event) {
</select>
</div>
<div class="col-span-2">
<label class="text-[11px] font-black text-slate-600 block">상위 위치 선택</label>
<select id="new-unit-parent" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none"></select>
<label class="member-form-label block">상위 위치 선택</label>
<select id="new-unit-parent" class="member-form-select"></select>
</div>
<div class="col-span-2">
<label class="text-[11px] font-black text-slate-600 block">신규 명칭 입력</label>
<input id="new-unit-name" placeholder="예: 신규개발팀" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
<label class="member-form-label block">신규 명칭 입력</label>
<input id="new-unit-name" placeholder="예: 신규개발팀" class="member-form-input">
</div>
`;
updateParentList();
document.getElementById('modal-footer-area').innerHTML = `
<button onclick="closeModal()" class="flex-1 bg-slate-100 py-3.5 rounded-xl font-bold text-sm">취소</button>
<button onclick="saveNewUnit()" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
<div class="modal-footer-actions">
<button onclick="closeModal()" class="modal-btn modal-btn-cancel">취소</button>
<button onclick="saveNewUnit()" class="modal-btn modal-btn-save">저장</button>
</div>
`;
modal.style.display = 'flex';
}
@@ -934,14 +936,16 @@ function openOrgEditModal(level, oldName) {
fieldsArea.style.maxHeight = 'none';
fieldsArea.innerHTML = `
<div class="col-span-2">
<label class="text-[11px] font-black text-slate-400 block">새로운 ${level} 명칭</label>
<input id="new-org-name" value="${oldName}" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
<label class="member-form-label block">새로운 ${level} 명칭</label>
<input id="new-org-name" value="${oldName}" class="member-form-input">
</div>
`;
document.getElementById('modal-footer-area').innerHTML = `
<button onclick="deleteOrg('${jsString(level)}', '${jsString(oldName)}')" class="bg-red-50 text-red-600 py-3.5 px-6 rounded-xl font-bold text-sm border border-red-100 hover:bg-red-100 transition-colors">삭제</button>
<button onclick="closeModal()" class="flex-1 bg-slate-100 py-3.5 rounded-xl font-bold text-sm">취소</button>
<button onclick="saveOrgName('${jsString(level)}', '${jsString(oldName)}')" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
<button onclick="deleteOrg('${jsString(level)}', '${jsString(oldName)}')" class="modal-btn modal-btn-delete">삭제</button>
<div class="modal-footer-actions">
<button onclick="closeModal()" class="modal-btn modal-btn-cancel">취소</button>
<button onclick="saveOrgName('${jsString(level)}', '${jsString(oldName)}')" class="modal-btn modal-btn-save">저장</button>
</div>
`;
modal.style.display = 'flex';
}
@@ -1102,12 +1106,8 @@ function switchModalTab(tab) {
const isBasic = tab === 'basic';
document.getElementById('modal-sec-basic').classList.toggle('hidden', !isBasic);
document.getElementById('modal-sec-org').classList.toggle('hidden', isBasic);
document.getElementById('modal-tab-basic').className = isBasic
? 'flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all'
: 'flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all';
document.getElementById('modal-tab-org').className = !isBasic
? 'flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all'
: 'flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all';
document.getElementById('modal-tab-basic').className = isBasic ? 'member-modal-tab is-active' : 'member-modal-tab';
document.getElementById('modal-tab-org').className = !isBasic ? 'member-modal-tab is-active' : 'member-modal-tab';
}
function openModal(id) {
@@ -1124,14 +1124,14 @@ function openModal(id) {
fieldsArea.style.maxHeight = 'none';
fieldsArea.innerHTML = `
<div class="member-detail-top-row">
<div class="relative w-32 h-32 rounded-full overflow-hidden border-4 border-indigo-100 shadow-lg">
<div class="relative w-32 h-32 rounded-full overflow-hidden border-4 shadow-lg" style="border-color: var(--color-surface-strong);">
<img src="${member['사진'] || 'https://via.placeholder.com/120?text=Profile'}" class="w-full h-full object-cover">
</div>
<div class="member-detail-summary">
<div>
<h2 class="text-2xl font-black text-slate-800">${member['이름'] || ''}</h2>
<p class="text-indigo-600 font-bold">${member['직급'] || '-'} / ${member['직책'] || '팀원'}</p>
<p class="text-slate-400 text-xs mt-1 font-medium">${(member._path || []).map((path) => path.name).join(' > ')}</p>
<p class="font-bold" style="color: var(--color-header);">${member['직급'] || '-'} / ${member['직책'] || '팀원'}</p>
<p class="text-xs mt-1 font-medium" style="color: var(--color-text-muted);">${(member._path || []).map((path) => path.name).join(' > ')}</p>
</div>
<div class="member-inline-info-grid">
<div class="member-inline-info-card">
@@ -1149,7 +1149,7 @@ function openModal(id) {
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
</div>
`;
footer.innerHTML = '<button onclick="closeModal()" class="w-full bg-slate-800 text-white py-4 rounded-xl font-bold text-sm shadow-lg">닫기</button>';
footer.innerHTML = '<button onclick="closeModal()" class="modal-btn modal-btn-close">닫기</button>';
modal.style.display = 'flex';
hydrateMemberSeatPreview(member);
return;
@@ -1168,14 +1168,14 @@ function openModal(id) {
const currentValue = member[field] || '';
orgFields += `
<div class="col-span-1">
<label class="text-[11px] font-black text-slate-600 block">${field}</label>
<select id="sel-${field}" onchange="toggleManualInput('${field}')" class="w-full bg-white p-3 rounded-xl border text-sm font-bold text-slate-700 outline-none">
<option value="__NEW__" class="text-indigo-600 font-bold">+ 직접/신규 입력</option>
<label class="member-form-label block">${field}</label>
<select id="sel-${field}" onchange="toggleManualInput('${field}')" class="member-form-select">
<option value="__NEW__" class="member-form-new-option">+ 직접/신규 입력</option>
<option value="__NONE__" ${currentValue === '' ? 'selected' : ''}>-- 선택 안 함 --</option>
${uniqueValues.map((value) => `<option value="${value}" ${value === currentValue ? 'selected' : ''}>${value}</option>`).join('')}
</select>
<div id="manual-${field}" class="hidden mt-2">
<input id="input-${field}" placeholder="직접 입력" class="w-full bg-indigo-50 p-3 rounded-xl border-indigo-200 border text-sm font-bold">
<div id="manual-${field}" class="hidden member-form-manual">
<input id="input-${field}" placeholder="직접 입력" class="member-form-input">
</div>
</div>
`;
@@ -1184,30 +1184,30 @@ function openModal(id) {
const isFlexible = member['근무시간'] === '유연근무제';
orgFields += `
<div class="col-span-1">
<label class="text-[11px] font-black text-slate-600 block">근무 상태</label>
<select id="m-status" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none">
<label class="member-form-label block">근무 상태</label>
<select id="m-status" class="member-form-select">
<option value="근무" ${member['근무상태'] !== '휴직' && member['근무상태'] !== '퇴직' ? 'selected' : ''}>근무</option>
<option value="휴직" ${member['근무상태'] === '휴직' ? 'selected' : ''}>휴직</option>
<option value="퇴직" ${member['근무상태'] === '퇴직' ? 'selected' : ''}>퇴직</option>
</select>
</div>
<div class="col-span-1">
<label class="text-[11px] font-black text-slate-600 block">근무 시간</label>
<select id="m-worktime" onchange="toggleFlexibleTime(this.value)" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none">
<label class="member-form-label block">근무 시간</label>
<select id="m-worktime" onchange="toggleFlexibleTime(this.value)" class="member-form-select">
<option value="09~18" ${!isFlexible ? 'selected' : ''}>09~18</option>
<option value="유연근무제" ${isFlexible ? 'selected' : ''}>유연근무제</option>
</select>
<div id="flexible-time-area" class="${isFlexible ? '' : 'hidden'} mt-2 flex items-center gap-2">
<input type="time" id="m-work-start" value="${member['유연근무_시작'] || '09:00'}" class="bg-indigo-50 p-2 rounded-lg border border-indigo-100 text-xs font-bold w-full">
<input type="time" id="m-work-end" value="${member['유연근무_종료'] || '18:00'}" class="bg-indigo-50 p-2 rounded-lg border border-indigo-100 text-xs font-bold w-full">
<input type="time" id="m-work-start" value="${member['유연근무_시작'] || '09:00'}" class="member-form-time">
<input type="time" id="m-work-end" value="${member['유연근무_종료'] || '18:00'}" class="member-form-time">
</div>
</div>
</div>`;
fieldsArea.innerHTML = `
<div class="flex border-b mb-6 sticky top-0 bg-white z-10">
<button id="modal-tab-basic" onclick="switchModalTab('basic')" class="flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all">기본 정보</button>
<button id="modal-tab-org" onclick="switchModalTab('org')" class="flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all">조직 및 근무</button>
<div class="member-modal-tabs">
<button id="modal-tab-basic" onclick="switchModalTab('basic')" class="member-modal-tab is-active">기본 정보</button>
<button id="modal-tab-org" onclick="switchModalTab('org')" class="member-modal-tab">조직 및 근무</button>
</div>
<div id="modal-sec-basic" class="modal-form-grid member-basic-editor">
<input type="hidden" id="m-id" value="${id || ''}">
@@ -1216,6 +1216,7 @@ function openModal(id) {
<div class="member-basic-split">
<div class="member-basic-left">
<div class="member-photo-panel">
<p class="member-modal-panel-title">기본 정보</p>
<div class="member-photo-upload-card member-photo-upload-card-inline">
<div class="member-photo-card-title">프로필 사진</div>
<div class="member-photo-preview-wrap">
@@ -1230,42 +1231,46 @@ function openModal(id) {
</div>
</div>
</div>
<div class="member-basic-fields">
<div class="member-basic-fields member-modal-panel">
<p class="member-modal-panel-title">기본 정보</p>
<div class="member-basic-field">
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
<label class="member-form-label block">이름 (필수)</label>
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="member-form-input">
</div>
<div class="member-basic-field">
<label class="text-[11px] font-black text-slate-600 block">사번</label>
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
<label class="member-form-label block">사번</label>
<input id="m-employee-id" value="${member['사번'] || ''}" class="member-form-input">
</div>
<div class="member-basic-field">
<label class="text-[11px] font-black text-slate-600 block">전화번호</label>
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
<label class="member-form-label block">전화번호</label>
<input id="m-phone" value="${member['전화번호'] || ''}" class="member-form-input">
</div>
<div class="member-basic-field">
<label class="text-[11px] font-black text-slate-600 block">이메일</label>
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
<label class="member-form-label block">이메일</label>
<input id="m-email" value="${member['이메일'] || ''}" class="member-form-input">
</div>
</div>
</div>
<div class="member-basic-right">
<p class="member-modal-panel-title" style="padding:16px 16px 0;">조직 및 근무</p>
<div class="member-seat-field member-seat-field-compact">
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
</div>
</div>
</div>
</div>
${orgFields}
<div class="member-modal-panel">${orgFields}</div>
`;
resetPhotoPreviewObjectUrl();
const deleteBtn = id ? `<button onclick="deleteMember('${id}')" class="bg-red-50 text-red-600 py-3.5 px-6 rounded-xl font-bold text-sm border border-red-100 hover:bg-red-100 transition-colors">삭제</button>` : '';
const deleteBtn = id ? `<button onclick="deleteMember('${id}')" class="modal-btn modal-btn-delete">삭제</button>` : '';
footer.innerHTML = `
${deleteBtn}
<button onclick="closeModal()" class="flex-1 bg-slate-100 py-3.5 rounded-xl font-bold text-sm">취소</button>
<button onclick="saveMember()" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
<div class="modal-footer-actions">
<button onclick="closeModal()" class="modal-btn modal-btn-cancel">취소</button>
<button onclick="saveMember()" class="modal-btn modal-btn-save">저장</button>
</div>
`;
modal.style.display = 'flex';
if (id) {
@@ -1420,8 +1425,8 @@ function openListViewModal(event) {
</div>
</div>
<div class="list-toolbar-row">
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-slate-50 border-2 border-slate-100 p-3 rounded-xl text-sm outline-none font-bold focus:border-indigo-400 transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
<button type="button" onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-indigo-600 text-white px-5 rounded-xl font-bold text-sm">검색</button>
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-[#f6eddd] border-2 border-[#e0d0b4] p-3 rounded-xl text-sm outline-none font-bold text-[#1f2f25] focus:border-[#d68a3a] transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
<button type="button" onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-[#214634] text-white px-5 rounded-xl font-bold text-sm">검색</button>
</div>
<div id="list-view-status" class="list-view-status"></div>
</div>
@@ -1453,19 +1458,19 @@ function renderListViewFooter() {
footer.innerHTML = `
<div class="flex gap-2 w-full justify-between items-center">
<div class="flex gap-2">
<button onclick="openAddModal(event)" class="bg-indigo-50 text-indigo-600 px-4 py-2 rounded-lg text-xs font-bold border border-indigo-100">+ 구성원 추가</button>
<button onclick="openUnitAddModal(event)" class="bg-indigo-50 text-indigo-600 px-4 py-2 rounded-lg text-xs font-bold border border-indigo-100">+ 조직 추가</button>
<button onclick="openAddModal(event)" class="bg-[#f6eddd] text-[#214634] px-4 py-2 rounded-lg text-xs font-bold border border-[#e0d0b4]">+ 구성원 추가</button>
<button onclick="openUnitAddModal(event)" class="bg-[#f6eddd] text-[#214634] px-4 py-2 rounded-lg text-xs font-bold border border-[#e0d0b4]">+ 조직 추가</button>
</div>
<div class="flex gap-2 items-center">
<p class="text-[10px] text-slate-400 font-bold mr-4">항목을 드래그하여 순서를 바꿀 수 있습니다.</p>
<button onclick="closeModal()" class="bg-slate-100 text-slate-600 px-6 py-2 rounded-lg text-xs font-bold">취소</button>
<button onclick="applyListViewChanges()" class="bg-indigo-600 text-white px-8 py-2 rounded-lg text-xs font-bold shadow-lg shadow-indigo-200">반영하기</button>
<p class="text-[10px] text-[#8b8a77] font-bold mr-4">항목을 드래그하여 순서를 바꿀 수 있습니다.</p>
<button onclick="closeModal()" class="bg-[#efe4d0] text-[#5b665a] px-6 py-2 rounded-lg text-xs font-bold">취소</button>
<button onclick="applyListViewChanges()" class="bg-[#214634] text-white px-8 py-2 rounded-lg text-xs font-bold shadow-lg shadow-[#d6c1a3]">반영하기</button>
</div>
</div>
`;
return;
}
footer.innerHTML = '<div class="flex gap-2 w-full justify-end items-center"><button onclick="closeModal()" class="bg-indigo-600 text-white px-10 py-2.5 rounded-lg text-xs font-bold shadow-lg shadow-indigo-200">닫기</button></div>';
footer.innerHTML = '<div class="flex gap-2 w-full justify-end items-center"><button onclick="closeModal()" class="bg-[#214634] text-white px-10 py-2.5 rounded-lg text-xs font-bold shadow-lg shadow-[#d6c1a3]">닫기</button></div>';
}
function getRenderableListMembers() {