feat(erp): ERP 대시보드 + 관리자 권한 시스템 도입
- 로그인: dbxcorp.co.kr 도메인 화이트리스트만 사용. ALLOWED_EMAILS 게이트 제거 - king@dbxcorp.co.kr 슈퍼관리자 자동 부여 (강등/삭제 불가) - 신규 로그인 사용자는 JSON 저장소에 자동 등록 (기본 모듈 권한 0) - 관리자 페이지(/admin): 역할/모듈별 토글로 직원 권한 수정 - 메인 대시보드를 ERP 시스템(monochromatic shadcn)으로 재설계 · 모듈: CORM, Order, 개인경비(준비중), 휴가(준비중) · 권한 없는 모듈은 '권한 없음' 배지 + 클릭 불가 - DESIGN.md 토큰 기반 styles → app/static/erp.css - docker-compose: dbx-main-data 볼륨 마운트 (DATA_DIR=/data) 으로 권한 JSON 영구 보관 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,595 @@
|
||||
/* ════════════════════════════════════════════════════
|
||||
ERP 대시보드 — DESIGN.md 기반 (Monochromatic / shadcn)
|
||||
════════════════════════════════════════════════════ */
|
||||
|
||||
/* Geist 폰트 — CDN. 실패시 Inter / 시스템 폰트 fallback */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono&family=Inter:wght@400;500;600&display=swap");
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-canvas-white: #ffffff;
|
||||
--color-ghost-gray: #f2f2f2;
|
||||
--color-subtle-ash: #e5e5e5;
|
||||
--color-midtone-gray: #737373;
|
||||
--color-rich-black: #0a0a0a;
|
||||
--color-deep-black: #000000;
|
||||
--color-callout-red: #c22b10;
|
||||
--color-success-green:#10c22b;
|
||||
|
||||
/* Typography */
|
||||
--font-geist: 'Geist', 'Inter', ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-geist-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo,
|
||||
Monaco, Consolas, monospace;
|
||||
|
||||
--text-caption: 12px;
|
||||
--text-body: 14px;
|
||||
--text-heading: 18px;
|
||||
--text-display: 48px;
|
||||
--leading-body: 1.43;
|
||||
|
||||
--tracking-heading: -0.45px;
|
||||
--tracking-display: -2.4px;
|
||||
|
||||
/* Spacing */
|
||||
--sp-4: 4px; --sp-6: 6px; --sp-8: 8px; --sp-10: 10px;
|
||||
--sp-12: 12px; --sp-16: 16px; --sp-20: 20px; --sp-24: 24px;
|
||||
--sp-32: 32px; --sp-40: 40px; --sp-80: 80px;
|
||||
|
||||
/* Radii */
|
||||
--r-pill: 9999px;
|
||||
--r-badge: 26px;
|
||||
--r-card: 14px;
|
||||
--r-input: 10px;
|
||||
--r-btn: 10px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-card: oklab(0.145 -0.00000143796 0.00000340492 / 0.1) 0px 0px 0px 1px;
|
||||
--shadow-focus: lab(100 0 0) 0px 0px 0px 2px;
|
||||
}
|
||||
|
||||
/* ── 리셋 ── (auth pages 와 충돌 방지 위해 erp-shell 안에서만 적용) */
|
||||
.erp-shell, .erp-shell * { box-sizing: border-box; }
|
||||
.erp-shell { margin: 0; padding: 0; }
|
||||
|
||||
body.erp-body {
|
||||
margin: 0;
|
||||
font-family: var(--font-geist);
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--color-rich-black);
|
||||
background: var(--color-canvas-white);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.erp-body a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* ── 상단 네비게이션 ── */
|
||||
.erp-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: var(--color-canvas-white);
|
||||
border-bottom: 1px solid var(--color-subtle-ash);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--sp-24);
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.erp-nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-16);
|
||||
}
|
||||
|
||||
.erp-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-8);
|
||||
font-weight: 600;
|
||||
color: var(--color-deep-black);
|
||||
font-size: var(--text-body);
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.erp-brand-logo {
|
||||
height: 36px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.erp-brand-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--color-subtle-ash);
|
||||
}
|
||||
|
||||
.erp-brand-system {
|
||||
font-size: var(--text-body);
|
||||
font-weight: 500;
|
||||
color: var(--color-rich-black);
|
||||
}
|
||||
|
||||
.erp-nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-8);
|
||||
}
|
||||
|
||||
.erp-user-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-8);
|
||||
padding: 4px var(--sp-12) 4px 4px;
|
||||
border: 1px solid var(--color-subtle-ash);
|
||||
border-radius: var(--r-pill);
|
||||
background: var(--color-canvas-white);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-rich-black);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.erp-user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.erp-user-avatar-fallback {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-deep-black);
|
||||
color: var(--color-canvas-white);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.erp-user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.erp-user-name {
|
||||
font-weight: 500;
|
||||
color: var(--color-rich-black);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.erp-user-email {
|
||||
color: var(--color-midtone-gray);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ── 버튼 ── */
|
||||
.erp-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--sp-6);
|
||||
font-family: inherit;
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-btn);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 120ms, border-color 120ms, color 120ms;
|
||||
}
|
||||
|
||||
.erp-btn-primary {
|
||||
background: var(--color-deep-black);
|
||||
color: var(--color-canvas-white);
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.erp-btn-primary:hover {
|
||||
background: var(--color-rich-black);
|
||||
}
|
||||
|
||||
.erp-btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-rich-black);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--r-pill);
|
||||
}
|
||||
|
||||
.erp-btn-ghost:hover {
|
||||
background: var(--color-ghost-gray);
|
||||
}
|
||||
|
||||
.erp-btn-outline {
|
||||
background: var(--color-canvas-white);
|
||||
color: var(--color-rich-black);
|
||||
border-color: var(--color-subtle-ash);
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.erp-btn-outline:hover {
|
||||
background: var(--color-ghost-gray);
|
||||
}
|
||||
|
||||
.erp-btn-danger {
|
||||
background: transparent;
|
||||
color: var(--color-callout-red);
|
||||
border-color: var(--color-subtle-ash);
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.erp-btn-danger:hover {
|
||||
border-color: var(--color-callout-red);
|
||||
background: rgba(194, 43, 16, 0.04);
|
||||
}
|
||||
|
||||
/* ── 본문 컨테이너 ── */
|
||||
.erp-main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--sp-40) var(--sp-24);
|
||||
}
|
||||
|
||||
.erp-greeting {
|
||||
margin: 0 0 var(--sp-8);
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--color-deep-black);
|
||||
letter-spacing: -0.6px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.erp-greeting-sub {
|
||||
margin: 0 0 var(--sp-32);
|
||||
color: var(--color-midtone-gray);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.erp-section-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--sp-16);
|
||||
}
|
||||
|
||||
.erp-section-title {
|
||||
font-size: var(--text-heading);
|
||||
font-weight: 500;
|
||||
color: var(--color-deep-black);
|
||||
letter-spacing: var(--tracking-heading);
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.erp-section-meta {
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-midtone-gray);
|
||||
}
|
||||
|
||||
/* ── 카드 그리드 ── */
|
||||
.erp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--sp-16);
|
||||
}
|
||||
|
||||
.erp-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-12);
|
||||
background: var(--color-canvas-white);
|
||||
border-radius: var(--r-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: var(--sp-20);
|
||||
min-height: 180px;
|
||||
transition: box-shadow 120ms, transform 120ms;
|
||||
}
|
||||
|
||||
.erp-card[data-clickable="true"]:hover {
|
||||
box-shadow: oklab(0.145 -0.00000143796 0.00000340492 / 0.18) 0 0 0 1px,
|
||||
oklab(0.145 -0.00000143796 0.00000340492 / 0.06) 0 8px 24px;
|
||||
}
|
||||
|
||||
.erp-card[data-clickable="false"] {
|
||||
background: var(--color-ghost-gray);
|
||||
}
|
||||
|
||||
.erp-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-8);
|
||||
}
|
||||
|
||||
.erp-card-title {
|
||||
font-size: var(--text-heading);
|
||||
font-weight: 600;
|
||||
color: var(--color-deep-black);
|
||||
letter-spacing: var(--tracking-heading);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.erp-card-subtitle {
|
||||
font-family: var(--font-geist-mono);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-midtone-gray);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.erp-card-desc {
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-midtone-gray);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.erp-card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-8);
|
||||
margin-top: auto;
|
||||
padding-top: var(--sp-12);
|
||||
border-top: 1px solid var(--color-subtle-ash);
|
||||
}
|
||||
|
||||
.erp-card-link {
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
color: var(--color-deep-black);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
|
||||
.erp-card-link svg {
|
||||
transition: transform 120ms;
|
||||
}
|
||||
|
||||
.erp-card[data-clickable="true"]:hover .erp-card-link svg {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
/* ── 배지 ── */
|
||||
.erp-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-6);
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--r-badge);
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.erp-badge-inverse {
|
||||
background: var(--color-deep-black);
|
||||
color: var(--color-canvas-white);
|
||||
}
|
||||
|
||||
.erp-badge-neutral {
|
||||
background: var(--color-ghost-gray);
|
||||
color: var(--color-rich-black);
|
||||
}
|
||||
|
||||
.erp-badge-outline {
|
||||
background: transparent;
|
||||
color: var(--color-rich-black);
|
||||
border: 1px solid #a1a1a1;
|
||||
}
|
||||
|
||||
.erp-badge-success { background: #e6f8e9; color: var(--color-success-green); border: 1px solid #c2efc8; }
|
||||
.erp-badge-danger { background: #fbe9e6; color: var(--color-callout-red); border: 1px solid #efc6bf; }
|
||||
|
||||
.erp-badge-dot::before {
|
||||
content: "";
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* ── 잠긴 카드 표시 ── */
|
||||
.erp-locked-overlay {
|
||||
position: absolute;
|
||||
top: var(--sp-12);
|
||||
right: var(--sp-12);
|
||||
font-size: 11px;
|
||||
color: var(--color-midtone-gray);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* ── 관리자 페이지 ── */
|
||||
.erp-admin-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-12);
|
||||
margin-bottom: var(--sp-16);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.erp-input {
|
||||
font-family: inherit;
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-rich-black);
|
||||
background: var(--color-canvas-white);
|
||||
border: 1px solid var(--color-subtle-ash);
|
||||
border-radius: var(--r-input);
|
||||
padding: 6px var(--sp-10);
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.erp-input:focus {
|
||||
outline: none;
|
||||
box-shadow: var(--shadow-focus);
|
||||
border-color: var(--color-deep-black);
|
||||
}
|
||||
|
||||
.erp-table-wrap {
|
||||
border-radius: var(--r-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
background: var(--color-canvas-white);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.erp-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.erp-table thead th {
|
||||
text-align: left;
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
color: var(--color-midtone-gray);
|
||||
background: var(--color-ghost-gray);
|
||||
padding: var(--sp-10) var(--sp-12);
|
||||
border-bottom: 1px solid var(--color-subtle-ash);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.erp-table tbody td {
|
||||
padding: var(--sp-12);
|
||||
border-bottom: 1px solid var(--color-subtle-ash);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.erp-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.erp-table tbody tr[data-dirty="true"] {
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.erp-user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-10);
|
||||
}
|
||||
|
||||
.erp-user-cell .erp-user-avatar,
|
||||
.erp-user-cell .erp-user-avatar-fallback {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.erp-user-cell-name {
|
||||
font-weight: 500;
|
||||
color: var(--color-rich-black);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.erp-user-cell-email {
|
||||
font-size: 12px;
|
||||
color: var(--color-midtone-gray);
|
||||
line-height: 1.2;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* shadcn-style 토글 (checkbox 기반) */
|
||||
.erp-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
}
|
||||
.erp-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.erp-switch-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--color-subtle-ash);
|
||||
border-radius: var(--r-pill);
|
||||
cursor: pointer;
|
||||
transition: background 120ms;
|
||||
}
|
||||
.erp-switch-slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px; height: 16px;
|
||||
left: 2px; top: 2px;
|
||||
background: var(--color-canvas-white);
|
||||
border-radius: 50%;
|
||||
transition: transform 120ms;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.2);
|
||||
}
|
||||
.erp-switch input:checked + .erp-switch-slider { background: var(--color-deep-black); }
|
||||
.erp-switch input:checked + .erp-switch-slider::before { transform: translateX(16px); }
|
||||
.erp-switch input:disabled + .erp-switch-slider {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* shadcn-style select */
|
||||
.erp-select {
|
||||
font-family: inherit;
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-rich-black);
|
||||
background: var(--color-canvas-white);
|
||||
border: 1px solid var(--color-subtle-ash);
|
||||
border-radius: var(--r-input);
|
||||
padding: 4px var(--sp-10);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.erp-select:focus {
|
||||
outline: none;
|
||||
box-shadow: var(--shadow-focus);
|
||||
border-color: var(--color-deep-black);
|
||||
}
|
||||
|
||||
.erp-select:disabled {
|
||||
background: var(--color-ghost-gray);
|
||||
cursor: not-allowed;
|
||||
color: var(--color-midtone-gray);
|
||||
}
|
||||
|
||||
/* 토스트 */
|
||||
.erp-toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
background: var(--color-deep-black);
|
||||
color: var(--color-canvas-white);
|
||||
padding: var(--sp-12) var(--sp-16);
|
||||
border-radius: var(--r-input);
|
||||
font-size: var(--text-body);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: opacity 200ms, transform 200ms;
|
||||
pointer-events: none;
|
||||
max-width: 360px;
|
||||
}
|
||||
.erp-toast[data-visible="true"] {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.erp-toast[data-type="error"] {
|
||||
background: var(--color-callout-red);
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 640px) {
|
||||
.erp-nav { padding: 0 var(--sp-16); height: 52px; }
|
||||
.erp-brand-system { display: none; }
|
||||
.erp-user-info { display: none; }
|
||||
.erp-main { padding: var(--sp-24) var(--sp-16); }
|
||||
.erp-greeting { font-size: 22px; }
|
||||
.erp-table thead { display: none; }
|
||||
.erp-table tbody td { display: block; border: none; padding: 4px var(--sp-12); }
|
||||
.erp-table tbody tr { display: block; padding: var(--sp-12) 0; border-bottom: 1px solid var(--color-subtle-ash); }
|
||||
}
|
||||
Reference in New Issue
Block a user