회사 관리 기능 확장 + 테넌트/비번 보안 하드닝

- 첫 로그인 비번 강제 변경 (RUN_082): FORCE_PASSWORD_CHANGE 컬럼,
  ForcePasswordChangeGuardFilter, /auth/change-password API + 페이지
- 테넌트 일관성 가드: TenantConsistencyGuardFilter 로 JWT.company_code
  ↔ 서브도메인 company_code 대조, CompanyResolver 가 (db_name, company_code)
  동시 반환
- 회사 관리 확장 (RUN_083 audit log, RUN_084 lifecycle 컬럼):
  CompanyAdmin/Members/Templates/Lifecycle/AuditLog 서비스 +
  CompanyMgmtController + SuperAdminGuard
- 회사 관리 UI: CompanyAccordionRow 탭화 + 모달 4종
  (AdminInfo/Deactivate/Delete/RecopyTemplates) + AuditLogDrawer + csvExport
- 프로비저닝 마법사: force_password_change 토글 반영
- 프론트 인증: storage 이벤트 멀티탭 동기화, 403 errorCode
  (PASSWORD_CHANGE_REQUIRED / CROSS_TENANT_REJECTED / TENANT_NOT_RESOLVED)
  전역 리다이렉트
- 기타: StartupSchemaMigrator, OS별 도커 기동 스크립트, CLAUDE.md 트래킹

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:36:05 +09:00
parent 06998cd2a5
commit 68f85f3736
58 changed files with 6110 additions and 316 deletions
+63 -30
View File
@@ -256,10 +256,9 @@ html:not(.dark) .v5-hdr{
box-shadow:0 0 6px rgba(var(--v5-pink-rgb),.8);
animation:v5-pdot 2s infinite;
}
/* Admin mode tint: when .v5-admin-mode, the mode-toggle glows cyan. */
.v5-admin-mode .v5-hdr-icon.v5-mode-toggle{
color:var(--v5-cyan);
background:rgba(var(--v5-cyan-rgb),.10);
color:var(--v5-primary);
background:rgba(var(--v5-primary-rgb),.10);
}
/* 대시보드 생성 버튼 (헤더, Light/Dark 토글 왼쪽) */
@@ -302,9 +301,9 @@ html:not(.dark) .v5-hdr{
.v5-admin-btn .ic-home{display:none;}
.v5-admin-mode .v5-admin-btn .ic-gear{display:none;}
.v5-admin-mode .v5-admin-btn .ic-home{display:block;}
.v5-admin-mode .v5-admin-btn{color:var(--v5-cyan);background:rgba(var(--v5-cyan-rgb),.10);}
.v5-admin-mode .v5-admin-btn:hover{color:var(--v5-cyan);background:rgba(var(--v5-cyan-rgb),.16);}
.v5-admin-mode .v5-admin-btn .v5-admin-label{color:var(--v5-cyan);}
.v5-admin-mode .v5-admin-btn{color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.10);}
.v5-admin-mode .v5-admin-btn:hover{color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.16);}
.v5-admin-mode .v5-admin-btn .v5-admin-label{color:var(--v5-primary);}
/* Avatar */
.v5-avatar-w{position:relative;}
@@ -376,10 +375,10 @@ html:not(.dark) .v5-hdr{
/* Admin badge — display:none 대신 opacity/transform 으로 hidden 해서 zoom-in/out 애니메이션 가능 */
.v5-admin-badge{display:flex;align-items:center;gap:.4rem;padding:.2rem .6rem;border-radius:999px;
background:linear-gradient(135deg,rgba(var(--v5-primary-rgb),.12),rgba(var(--v5-cyan-rgb),.08));
background:linear-gradient(135deg,rgba(var(--v5-primary-rgb),.14),rgba(var(--v5-primary-rgb),.06));
border:1px solid rgba(var(--v5-primary-rgb),.2);font-size:.58rem;font-weight:700;color:var(--v5-primary);
opacity:0;transform:scale(0) rotate(-30deg);pointer-events:none;}
.dark .v5-admin-badge{background:linear-gradient(135deg,rgba(var(--v5-primary-rgb),.12),rgba(var(--v5-cyan-rgb),.08));
.dark .v5-admin-badge{background:linear-gradient(135deg,rgba(var(--v5-primary-rgb),.14),rgba(var(--v5-primary-rgb),.06));
border-color:rgba(var(--v5-primary-rgb),.2);color:var(--v5-primary-light);}
.v5-admin-mode .v5-admin-badge{opacity:1;transform:scale(1) rotate(0);pointer-events:auto;}
/* badge zoom — 모드 진입 시 bouncy in, 이탈 시 quick out */
@@ -392,14 +391,16 @@ html:not(.dark) .v5-hdr{
@keyframes v5-badge-zoom-out{
0%{opacity:1;transform:scale(1) rotate(0)}
100%{opacity:0;transform:scale(0) rotate(30deg)}}
.v5-admin-badge .badge-dot{width:6px;height:6px;border-radius:50%;background:var(--v5-cyan);
box-shadow:0 0 8px var(--v5-cyan-glow);animation:v5-bdPulse 2s infinite;}
@keyframes v5-bdPulse{0%,100%{box-shadow:0 0 4px var(--v5-cyan-glow)}50%{box-shadow:0 0 12px var(--v5-cyan-glow)}}
.v5-admin-badge .badge-dot{width:6px;height:6px;border-radius:50%;background:var(--v5-primary);
box-shadow:0 0 8px var(--v5-primary-glow);animation:v5-bdPulse 2s infinite;}
@keyframes v5-bdPulse{0%,100%{box-shadow:0 0 4px var(--v5-primary-glow)}50%{box-shadow:0 0 12px var(--v5-primary-glow)}}
/* ===== SOLID TABS ===== */
.v5-tabs{height:36px;display:flex;align-items:stretch;padding:0 .5rem;gap:1px;overflow-x:auto;
background:var(--v5-surface-solid);
border-bottom:1px solid var(--v5-border);position:relative;z-index:15;flex-shrink:0;}
border-bottom:1px solid var(--v5-border);position:relative;z-index:15;flex-shrink:0;
scrollbar-width:none;-ms-overflow-style:none;}
.v5-tabs::-webkit-scrollbar{display:none;}
.v5-tab{display:flex;align-items:center;gap:.4rem;padding:0 .85rem;font-size:.7rem;font-weight:500;
color:var(--v5-text-muted);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .25s;}
.v5-tab:hover{color:var(--v5-text-sec);background:var(--v5-surface-hover);}
@@ -514,9 +515,8 @@ html:not(.dark) .v5-side{
/* Content area stretches smoothly with sidebar */
.v5-body .v5-content{transition:all .5s cubic-bezier(.4,0,.2,1);}
.v5-side.collapsed{width:56px;padding:.85rem .4rem;overflow:visible;z-index:30;
border-right-color:var(--v5-primary);box-shadow:var(--v5-glow-sm);}
.v5-side.collapsed .v5-side-toggle{box-shadow:var(--v5-glow-sm);border-color:var(--v5-primary);color:var(--v5-primary);}
.v5-side.collapsed{width:56px;padding:.85rem .4rem;overflow:visible;z-index:30;}
.v5-side.collapsed .v5-side-toggle{color:var(--v5-primary);}
/* Collapsed menu items — center icon */
.v5-side.collapsed .v5-si{justify-content:center;padding:.55rem;border-radius:10px;gap:0;position:relative;
@@ -529,6 +529,18 @@ html:not(.dark) .v5-side{
.v5-side.collapsed .v5-si:nth-child(6){animation-delay:.28s;}
.v5-side.collapsed .v5-si:nth-child(7){animation-delay:.32s;}
.v5-side.collapsed .v5-si:nth-child(8){animation-delay:.36s;}
.v5-side.collapsed .v5-si:nth-child(9){animation-delay:.40s;}
.v5-side.collapsed .v5-si:nth-child(10){animation-delay:.44s;}
.v5-side.collapsed .v5-si:nth-child(11){animation-delay:.48s;}
.v5-side.collapsed .v5-si:nth-child(12){animation-delay:.52s;}
.v5-side.collapsed .v5-si:nth-child(13){animation-delay:.56s;}
.v5-side.collapsed .v5-si:nth-child(14){animation-delay:.60s;}
.v5-side.collapsed .v5-si:nth-child(15){animation-delay:.64s;}
.v5-side.collapsed .v5-si:nth-child(16){animation-delay:.68s;}
.v5-side.collapsed .v5-si:nth-child(17){animation-delay:.72s;}
.v5-side.collapsed .v5-si:nth-child(18){animation-delay:.76s;}
.v5-side.collapsed .v5-si:nth-child(19){animation-delay:.80s;}
.v5-side.collapsed .v5-si:nth-child(20){animation-delay:.84s;}
@keyframes v5-iconPop{from{opacity:0;transform:scale(.5)}to{opacity:1;transform:scale(1)}}
/* Hide text when collapsed */
@@ -564,6 +576,18 @@ html:not(.dark) .v5-side{
.v5-side:not(.collapsed) .v5-si:nth-child(6){animation-delay:.2s;}
.v5-side:not(.collapsed) .v5-si:nth-child(7){animation-delay:.23s;}
.v5-side:not(.collapsed) .v5-si:nth-child(8){animation-delay:.26s;}
.v5-side:not(.collapsed) .v5-si:nth-child(9){animation-delay:.29s;}
.v5-side:not(.collapsed) .v5-si:nth-child(10){animation-delay:.32s;}
.v5-side:not(.collapsed) .v5-si:nth-child(11){animation-delay:.35s;}
.v5-side:not(.collapsed) .v5-si:nth-child(12){animation-delay:.38s;}
.v5-side:not(.collapsed) .v5-si:nth-child(13){animation-delay:.41s;}
.v5-side:not(.collapsed) .v5-si:nth-child(14){animation-delay:.44s;}
.v5-side:not(.collapsed) .v5-si:nth-child(15){animation-delay:.47s;}
.v5-side:not(.collapsed) .v5-si:nth-child(16){animation-delay:.50s;}
.v5-side:not(.collapsed) .v5-si:nth-child(17){animation-delay:.53s;}
.v5-side:not(.collapsed) .v5-si:nth-child(18){animation-delay:.56s;}
.v5-side:not(.collapsed) .v5-si:nth-child(19){animation-delay:.59s;}
.v5-side:not(.collapsed) .v5-si:nth-child(20){animation-delay:.62s;}
@keyframes v5-menuSlideIn{from{opacity:0;transform:translateX(-12px)}to{opacity:1;transform:none}}
.v5-side:not(.collapsed) .v5-side-sec{opacity:1;
transition:opacity .35s .1s,height .35s .05s,padding .35s .05s;}
@@ -650,6 +674,19 @@ html:not(.dark) .v5-side{
.v5-side-flyout .fly-item:nth-child(5){animation-delay:.12s;}
.v5-side-flyout .fly-item:nth-child(6){animation-delay:.15s;}
.v5-side-flyout .fly-item:nth-child(7){animation-delay:.18s;}
.v5-side-flyout .fly-item:nth-child(8){animation-delay:.21s;}
.v5-side-flyout .fly-item:nth-child(9){animation-delay:.24s;}
.v5-side-flyout .fly-item:nth-child(10){animation-delay:.27s;}
.v5-side-flyout .fly-item:nth-child(11){animation-delay:.30s;}
.v5-side-flyout .fly-item:nth-child(12){animation-delay:.33s;}
.v5-side-flyout .fly-item:nth-child(13){animation-delay:.36s;}
.v5-side-flyout .fly-item:nth-child(14){animation-delay:.39s;}
.v5-side-flyout .fly-item:nth-child(15){animation-delay:.42s;}
.v5-side-flyout .fly-item:nth-child(16){animation-delay:.45s;}
.v5-side-flyout .fly-item:nth-child(17){animation-delay:.48s;}
.v5-side-flyout .fly-item:nth-child(18){animation-delay:.51s;}
.v5-side-flyout .fly-item:nth-child(19){animation-delay:.54s;}
.v5-side-flyout .fly-item:nth-child(20){animation-delay:.57s;}
@keyframes v5-flyItemIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
.v5-side-flyout .fly-title{font-size:.58rem;font-weight:700;color:var(--v5-text-muted);
text-transform:uppercase;letter-spacing:.08em;padding:.3rem .6rem .45rem;}
@@ -663,12 +700,12 @@ html:not(.dark) .v5-side{
.v5-side-flyout .fly-item.on .ic{opacity:1;}
/* Admin sidebar accent */
.v5-admin-side .v5-si.on{background:linear-gradient(135deg,rgba(var(--v5-cyan-rgb),.12),rgba(var(--v5-cyan-rgb),.05));
color:var(--v5-cyan);border-color:rgba(var(--v5-cyan-rgb),.2);}
.v5-admin-side .v5-si.on{background:linear-gradient(135deg,rgba(var(--v5-primary-rgb),.12),rgba(var(--v5-primary-rgb),.05));
color:var(--v5-primary);border-color:rgba(var(--v5-primary-rgb),.2);}
.v5-admin-side .v5-si.on .ic{opacity:1;}
.v5-admin-side .v5-si::before{background:var(--v5-cyan);}
.dark .v5-admin-side .v5-si.on{background:linear-gradient(135deg,rgba(var(--v5-cyan-rgb),.12),rgba(var(--v5-cyan-rgb),.05));
border-color:rgba(var(--v5-cyan-rgb),.15);}
.v5-admin-side .v5-si::before{background:var(--v5-primary);}
.dark .v5-admin-side .v5-si.on{background:linear-gradient(135deg,rgba(var(--v5-primary-rgb),.12),rgba(var(--v5-primary-rgb),.05));
border-color:rgba(var(--v5-primary-rgb),.15);}
/* ===== MODE TRANSITION ===== */
.v5-mode-fade{position:fixed;inset:0;z-index:9998;pointer-events:none;opacity:0;
@@ -699,7 +736,7 @@ html:not(.dark) .v5-side{
.v5-hdr-glow{position:absolute;bottom:-1px;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--v5-primary),transparent);
opacity:0;pointer-events:none;}
.v5-admin-mode .v5-hdr-glow{background:linear-gradient(90deg,transparent,var(--v5-cyan),transparent);}
.v5-admin-mode .v5-hdr-glow{background:linear-gradient(90deg,transparent,var(--v5-primary),transparent);}
.v5-hdr-glow.mode-flash{animation:v5-mode-hdr-flash 1.4s cubic-bezier(.16,1,.3,1) forwards;}
@keyframes v5-mode-hdr-flash{
0%{opacity:0;height:1px;filter:blur(0)}
@@ -717,15 +754,13 @@ html:not(.dark) .v5-side{
from{opacity:0;transform:translateY(8px) scale(.9);filter:blur(4px)}
to{opacity:1;transform:translateY(0) scale(1);filter:blur(0)}}
/* ===== MODE TRANSITION — toggle button burst (디자인시스템 mode-burst 포팅) =====
JS 가 .v5-mode-burst 컨테이너를 fixed 위치(클릭점)에 append.
기본 = primary(보라, → 사용자 모드), .admin = cyan(시안, → 관리자 모드). */
/* ===== MODE TRANSITION — toggle button burst ===== */
.v5-mode-burst{
position:fixed;width:0;height:0;
pointer-events:none;z-index:9998;
--burst-rgb:var(--v5-primary-rgb);
}
.v5-mode-burst.admin{--burst-rgb:var(--v5-cyan-rgb);}
.v5-mode-burst.admin{--burst-rgb:var(--v5-primary-rgb);}
/* Center expanding ring */
.v5-mode-burst .burst-ring{
@@ -768,11 +803,9 @@ html:not(.dark) .v5-side{
content:'';position:absolute;inset:0;
background:linear-gradient(90deg,
transparent 0%,
rgba(var(--v5-cyan-rgb),0) 15%,
rgba(var(--v5-cyan-rgb),.9) 40%,
rgba(var(--v5-primary-rgb),1) 50%,
rgba(var(--v5-pink-rgb),.9) 60%,
rgba(var(--v5-cyan-rgb),0) 85%,
rgba(var(--v5-primary-rgb),0) 15%,
rgba(var(--v5-primary-rgb),.95) 50%,
rgba(var(--v5-primary-rgb),0) 85%,
transparent 100%);
filter:blur(.5px);
transform:translateX(-100%);