diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx index 16b52fdf..e2b348b8 100644 --- a/frontend/app/(auth)/login/page.tsx +++ b/frontend/app/(auth)/login/page.tsx @@ -129,7 +129,7 @@ export default function LoginPage() {
-

INVION

+

Invy.one

Cosmic Command Center
@@ -157,7 +157,7 @@ export default function LoginPage() {
{error || "아이디 또는 비밀번호를 확인해주세요"}
-
© 2026 INVION
+
© 2026 Invy.one
); diff --git a/frontend/app/(main)/main/page.tsx b/frontend/app/(main)/main/page.tsx index c1290093..9f847608 100644 --- a/frontend/app/(main)/main/page.tsx +++ b/frontend/app/(main)/main/page.tsx @@ -63,7 +63,7 @@ export default function MainPage() {

플랫폼

-

WACE ERP/PLM

+

Invyone ERP/PLM

diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx index b7befe6a..a21a9d31 100644 --- a/frontend/app/(main)/page.tsx +++ b/frontend/app/(main)/page.tsx @@ -63,7 +63,7 @@ export default function MainHomePage() {

플랫폼

-

WACE ERP/PLM

+

Invyone ERP/PLM

diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 7fb69e1d..c884bd10 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -855,8 +855,9 @@ select { mask-repeat: repeat, no-repeat; mask-position: 0 0, 0 0; /* ease-in-out-cubic — 대칭형 곡선으로 시간/progress 가 거의 linear 에 가깝게 진행됨. - 1800ms 중 약 1260ms 가 시각적 reveal 에 쓰임 — 빠르지도 늦지도 않은 sweet spot. */ - animation: vt-soft-reveal 1800ms cubic-bezier(0.65, 0, 0.35, 1) forwards; + duration 은 themeTransition.ts 가 방향에 따라 --vt-duration 변수로 지정. + 검정→대비 강해서 빨라 보이는 perceived speed 비대칭을 보정하기 위해 dark 방향은 더 김. */ + animation: vt-soft-reveal var(--vt-duration, 1800ms) cubic-bezier(0.65, 0, 0.35, 1) forwards; } @keyframes vt-soft-reveal { diff --git a/frontend/components/admin/CompanySwitcher.tsx b/frontend/components/admin/CompanySwitcher.tsx index e7890897..2643de95 100644 --- a/frontend/components/admin/CompanySwitcher.tsx +++ b/frontend/components/admin/CompanySwitcher.tsx @@ -20,9 +20,9 @@ interface CompanySwitcherProps { } /** - * WACE 관리자 전용: 회사 선택 및 전환 컴포넌트 + * Invyone 관리자 전용: 회사 선택 및 전환 컴포넌트 * - * - WACE 관리자(company_code = "*", userType = "SUPER_ADMIN")만 표시 + * - Invyone 관리자(company_code = "*", userType = "SUPER_ADMIN")만 표시 * - 회사 선택 시 해당 회사로 전환하여 시스템 사용 * - JWT 토큰 재발급으로 company_code 변경 */ @@ -33,15 +33,15 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp const [searchText, setSearchText] = useState(""); const [loading, setLoading] = useState(false); - // WACE 관리자 권한 체크 (user_type만 확인) - const isWaceAdmin = user?.user_type === "SUPER_ADMIN"; + // Invyone 관리자 권한 체크 (user_type만 확인) + const isInvyoneAdmin = user?.user_type === "SUPER_ADMIN"; // 현재 선택된 회사명 표시 const currentCompanyName = React.useMemo(() => { if (!user?.company_code) return "로딩 중..."; if (user.company_code === "*") { - return "WACE (최고 관리자)"; + return "Invyone (최고 관리자)"; } // companies 배열에서 현재 회사 찾기 @@ -51,10 +51,10 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp // 회사 목록 조회 useEffect(() => { - if (isWaceAdmin && isOpen) { + if (isInvyoneAdmin && isOpen) { fetchCompanies(); } - }, [isWaceAdmin, isOpen]); + }, [isInvyoneAdmin, isOpen]); // 검색 필터링 useEffect(() => { @@ -75,24 +75,24 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp const response = await apiClient.get("/admin/companies/db"); if (response.data.success) { - // 활성 상태의 회사만 필터링 + company_code="*" 제외 (WACE는 별도 추가) + // 활성 상태의 회사만 필터링 + company_code="*" 제외 (Invyone는 별도 추가) const activeCompanies = response.data.data .filter((c: Company) => c.company_code !== "*") // DB의 "*" 제외 .filter((c: Company) => c.status === "active" || !c.status) .sort((a: Company, b: Company) => a.company_name.localeCompare(b.company_name)); - // WACE 복귀 옵션 추가 - const companiesWithWace: Company[] = [ + // Invyone 복귀 옵션 추가 + const companiesWithInvyone: Company[] = [ { company_code: "*", - company_name: "WACE (최고 관리자)", + company_name: "Invyone (최고 관리자)", status: "active", }, ...activeCompanies, ]; - setCompanies(companiesWithWace); - setFilteredCompanies(companiesWithWace); + setCompanies(companiesWithInvyone); + setFilteredCompanies(companiesWithInvyone); } } catch (error) { logger.error("회사 목록 조회 실패", error); @@ -124,8 +124,8 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp } }; - // WACE 관리자가 아니면 렌더링하지 않음 - if (!isWaceAdmin) { + // Invyone 관리자가 아니면 렌더링하지 않음 + if (!isInvyoneAdmin) { return null; } diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index cbd8a30d..a2e3f6fd 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -281,7 +281,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { const companyCode = (user as ExtendedUserInfo)?.company_code; if (companyCode === "*") { - setCurrentCompanyName("WACE (최고 관리자)"); + setCurrentCompanyName("Invyone (최고 관리자)"); } else if (companyCode) { try { const response = await apiClient.get("/admin/companies/db"); @@ -455,28 +455,84 @@ function AppLayoutInner({ children }: AppLayoutProps) { toast.warning("이 메뉴에 할당된 화면이 없습니다. 메뉴 설정을 확인해주세요."); }; - const handleModeSwitch = useCallback(() => { + const handleModeSwitch = useCallback((e?: React.MouseEvent) => { if (modeTransition !== "idle") return; - // 레퍼런스 invion-layout-v5.html switchMode() 를 그대로 따름: - // Phase 1 (0ms) : mode-fade 오버레이 페이드인 + 사이드바 slide-out + 탭 fade-out - // Phase 2 (300ms) : 모드 swap (React 재렌더) → 새 사이드바/탭 등장 - // Phase 3 (600ms) : 오버레이 페이드아웃 + 정리 - const fade = document.getElementById("v5-mode-fade"); + // 강화된 mode transition — 옵션 b/c/e/f 적용 (d 버튼 burst 제거, mode-fade overlay 도 제거): + // Phase 1 (0ms): 사이드바 items morph-out (stagger), 헤더 glow flash, + // breadcrumb swap-out, 이탈 시 admin badge zoom-out + // Phase 2 (350ms): React 가 모드 swap → 새 메뉴 morph-in (stagger), breadcrumb swap-in, + // 진입 시 admin badge zoom-in + // Phase 3 (~950ms): 모든 클래스 정리, idle 복귀 + const goingToAdmin = !isAdminMode; setModeTransition("out"); - fade?.classList.add("in"); + + // (b) 사이드바 items morph-out — stagger + const oldItems = Array.from(document.querySelectorAll(".v5-side .v5-si")); + oldItems.forEach((it, i) => { + it.style.animationDelay = `${i * 35}ms`; + it.classList.add("mode-morph-out"); + }); + + // (c) 헤더 glow line flash + const hdrGlow = document.querySelector(".v5-hdr-glow"); + if (hdrGlow) { + hdrGlow.classList.remove("mode-flash"); + void hdrGlow.offsetWidth; // reflow → 재시작 가능하게 + hdrGlow.classList.add("mode-flash"); + } + + // (d) 토글 버튼 burst 효과는 제거됨 — 가운데 밝아지는 게 거슬려서 통째로 뺌 + + // (e) breadcrumb swap-out + const bc = document.querySelector(".v5-hdr-bc"); + bc?.classList.remove("mode-swap-in"); + bc?.classList.add("mode-swap-out"); + + // (f) admin badge zoom-out (이탈 시에만) + if (!goingToAdmin) { + const badge = document.querySelector(".v5-admin-badge"); + badge?.classList.remove("mode-zoom-in"); + badge?.classList.add("mode-zoom-out"); + } setTimeout(() => { - // Phase 2: swap mode — React re-renders with new menus/tabs + // Phase 2: 모드 swap → React 재렌더 (새 메뉴/탭/breadcrumb) setTabMode(isAdminMode ? "user" : "admin"); setModeTransition("in"); - // Phase 3: 오버레이 페이드아웃 후 정리 + // 다음 프레임에 새 items 에 morph-in 적용 + requestAnimationFrame(() => { + const newItems = Array.from(document.querySelectorAll(".v5-side .v5-si")); + newItems.forEach((it, i) => { + it.style.animationDelay = `${i * 45}ms`; + it.classList.add("mode-morph-in"); + }); + + // breadcrumb swap-in (텍스트는 이미 새 모드 기준) + const newBc = document.querySelector(".v5-hdr-bc"); + newBc?.classList.remove("mode-swap-out"); + newBc?.classList.add("mode-swap-in"); + + // (f) admin badge zoom-in (진입 시에만) + if (goingToAdmin) { + const newBadge = document.querySelector(".v5-admin-badge"); + newBadge?.classList.remove("mode-zoom-out"); + newBadge?.classList.add("mode-zoom-in"); + } + }); + + // Phase 3: 모든 애니메이션 클래스 정리 setTimeout(() => { - fade?.classList.remove("in"); setModeTransition("idle"); - }, 300); - }, 300); + document.querySelectorAll(".v5-side .v5-si").forEach((it) => { + it.classList.remove("mode-morph-in", "mode-morph-out"); + it.style.animationDelay = ""; + }); + document.querySelector(".v5-hdr-bc")?.classList.remove("mode-swap-in", "mode-swap-out"); + document.querySelector(".v5-admin-badge")?.classList.remove("mode-zoom-in", "mode-zoom-out"); + }, 600); + }, 350); }, [isAdminMode, setTabMode, modeTransition]); const handleLogout = async () => { @@ -727,9 +783,6 @@ function AppLayoutInner({ children }: AppLayoutProps) { {/* Theme fade overlay */}
- {/* Mode transition fade overlay — radial gradient 으로 화면을 한 번 덮어 사이드바/탭 swap 을 가림 */} -
- {/* V5 Shell */}
{/* ===== Glass Header ===== */} @@ -739,7 +792,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { -
INVION
+
Invy.one
{isAdminMode ? "관리자" : "홈"} › {breadcrumbText}
@@ -748,6 +801,8 @@ function AppLayoutInner({ children }: AppLayoutProps) { 관리자 모드
+ {/* mode transition 헤더 glow 라인 — 평소엔 opacity 0, mode change 시에만 flash */} +
{/* Theme pill */}
@@ -773,9 +828,9 @@ function AppLayoutInner({ children }: AppLayoutProps) {
- {/* Admin toggle (gear ↔ home) */} + {/* Admin toggle (gear ↔ home) — 이벤트 전달해서 burst origin 으로 사용 */} {isAdmin && ( - )} {displayVisible.map((tab, i) => renderTab(tab, i))} diff --git a/frontend/constants/layout.ts b/frontend/constants/layout.ts index c04f5605..24ea77d4 100644 --- a/frontend/constants/layout.ts +++ b/frontend/constants/layout.ts @@ -3,7 +3,7 @@ */ export const LAYOUT_CONFIG = { - COMPANY_NAME: "WACE 솔루션", + COMPANY_NAME: "Invyone 솔루션", API_BASE_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8081/api", ENDPOINTS: { diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index d090cf0d..3ab08fee 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -180,7 +180,7 @@ export const useAuth = () => { }, [fetchCurrentUser, checkAuthStatus]); /** - * 회사 전환 처리 (WACE 관리자 전용) + * 회사 전환 처리 (Invyone 관리자 전용) */ const switchCompany = useCallback( async (companyCode: string): Promise<{ success: boolean; message: string }> => { diff --git a/frontend/lib/registry/components/conditional-container/index.ts b/frontend/lib/registry/components/conditional-container/index.ts index f195b5bc..d7bdc204 100644 --- a/frontend/lib/registry/components/conditional-container/index.ts +++ b/frontend/lib/registry/components/conditional-container/index.ts @@ -13,7 +13,7 @@ export const ConditionalContainerDefinition = { description: "셀렉트박스 값에 따라 다른 UI를 표시하는 조건부 컨테이너", icon: "GitBranch", version: "1.0.0", - author: "WACE", + author: "Invyone", tags: ["조건부", "분기", "동적", "레이아웃"], default_size: { diff --git a/frontend/lib/registry/components/section-card/index.ts b/frontend/lib/registry/components/section-card/index.ts index 2871462c..c3b82bb8 100644 --- a/frontend/lib/registry/components/section-card/index.ts +++ b/frontend/lib/registry/components/section-card/index.ts @@ -32,7 +32,7 @@ export const SectionCardDefinition = createComponentDefinition({ icon: "LayoutPanelTop", tags: ["섹션", "그룹", "카드", "컨테이너", "제목", "card"], version: "1.0.0", - author: "WACE", + author: "Invyone", hidden: true, // v2-section-card 사용으로 패널에서 숨김 }); diff --git a/frontend/lib/registry/components/section-paper/index.ts b/frontend/lib/registry/components/section-paper/index.ts index ff3e5e0b..6432ad05 100644 --- a/frontend/lib/registry/components/section-paper/index.ts +++ b/frontend/lib/registry/components/section-paper/index.ts @@ -29,7 +29,7 @@ export const SectionPaperDefinition = createComponentDefinition({ icon: "Square", tags: ["섹션", "그룹", "배경", "컨테이너", "색종이", "paper"], version: "1.0.0", - author: "WACE", + author: "Invyone", hidden: true, // v2-section-paper 사용으로 패널에서 숨김 }); diff --git a/frontend/lib/registry/components/table-search-widget/index.tsx b/frontend/lib/registry/components/table-search-widget/index.tsx index 5fd8bb86..05ff08df 100644 --- a/frontend/lib/registry/components/table-search-widget/index.tsx +++ b/frontend/lib/registry/components/table-search-widget/index.tsx @@ -22,7 +22,7 @@ ComponentRegistry.registerComponent({ renderer: TableSearchWidgetRenderer.render as any, config_panel: TableSearchWidgetConfigPanel, version: "1.0.0", - author: "WACE", + author: "Invyone", hidden: true, // v2-table-search-widget 사용으로 패널에서 숨김 }); diff --git a/frontend/lib/registry/components/v2-section-card/index.ts b/frontend/lib/registry/components/v2-section-card/index.ts index ab5d9623..214a0836 100644 --- a/frontend/lib/registry/components/v2-section-card/index.ts +++ b/frontend/lib/registry/components/v2-section-card/index.ts @@ -32,7 +32,7 @@ export const V2SectionCardDefinition = createComponentDefinition({ icon: "LayoutPanelTop", tags: ["섹션", "그룹", "카드", "컨테이너", "제목", "card"], version: "1.0.0", - author: "WACE", + author: "Invyone", }); // 컴포넌트는 SectionCardRenderer에서 자동 등록됩니다 diff --git a/frontend/lib/registry/components/v2-section-paper/index.ts b/frontend/lib/registry/components/v2-section-paper/index.ts index 3890d1b8..1afa9f50 100644 --- a/frontend/lib/registry/components/v2-section-paper/index.ts +++ b/frontend/lib/registry/components/v2-section-paper/index.ts @@ -29,7 +29,7 @@ export const V2SectionPaperDefinition = createComponentDefinition({ icon: "Square", tags: ["섹션", "그룹", "배경", "컨테이너", "색종이", "paper"], version: "1.0.0", - author: "WACE", + author: "Invyone", }); // 컴포넌트는 SectionPaperRenderer에서 자동 등록됩니다 diff --git a/frontend/lib/registry/components/v2-table-search-widget/index.tsx b/frontend/lib/registry/components/v2-table-search-widget/index.tsx index 24ca4f5e..c5b5a9f2 100644 --- a/frontend/lib/registry/components/v2-table-search-widget/index.tsx +++ b/frontend/lib/registry/components/v2-table-search-widget/index.tsx @@ -21,7 +21,7 @@ ComponentRegistry.registerComponent({ renderer: TableSearchWidgetRenderer.render as any, config_panel: V2TableSearchWidgetConfigPanel, version: "1.0.0", - author: "WACE", + author: "Invyone", }); export { TableSearchWidget } from "./TableSearchWidget"; diff --git a/frontend/lib/themeTransition.ts b/frontend/lib/themeTransition.ts index 1373803e..a6cbe574 100644 --- a/frontend/lib/themeTransition.ts +++ b/frontend/lib/themeTransition.ts @@ -48,6 +48,9 @@ export function animatedThemeChange( root.style.setProperty("--reveal-x", `${x}px`); root.style.setProperty("--reveal-y", `${y}px`); root.style.setProperty("--reveal-max", `${endRadius}px`); + // Perceived speed 보정: 다크로 갈 땐 검정 컨트라스트가 강해서 같은 duration 도 빠르게 느껴짐. + // dark 방향만 약 25% 더 길게 잡아 두 방향이 비슷한 속도로 인지되도록 함. + root.style.setProperty("--vt-duration", next === "dark" ? "2200ms" : "1700ms"); // View Transitions API 미지원 → 즉시 적용 if (!doc.startViewTransition) { diff --git a/frontend/styles/v5-layout.css b/frontend/styles/v5-layout.css index 02083ba6..aafe3041 100644 --- a/frontend/styles/v5-layout.css +++ b/frontend/styles/v5-layout.css @@ -191,15 +191,24 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo background:linear-gradient(90deg,var(--v5-primary),var(--v5-cyan));animation:v5-glowLine .6s ease-out both;} @keyframes v5-glowLine{from{transform:scaleX(0);opacity:0}to{transform:scaleX(1);opacity:1}} -/* Admin badge */ -.v5-admin-badge{display:none;align-items:center;gap:.4rem;padding:.2rem .6rem;border-radius:999px; +/* 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(108,92,231,.12),rgba(0,206,201,.08)); border:1px solid rgba(108,92,231,.2);font-size:.58rem;font-weight:700;color:var(--v5-primary); - animation:v5-badgeIn .4s cubic-bezier(.16,1,.3,1) both;} + opacity:0;transform:scale(0) rotate(-30deg);pointer-events:none;} .dark .v5-admin-badge{background:linear-gradient(135deg,rgba(162,155,254,.12),rgba(85,239,196,.08)); border-color:rgba(162,155,254,.2);color:var(--v5-primary-light);} -.v5-admin-mode .v5-admin-badge{display:flex;} -@keyframes v5-badgeIn{from{opacity:0;transform:scale(.8) translateX(-10px)}to{opacity:1;transform:none}} +.v5-admin-mode .v5-admin-badge{opacity:1;transform:scale(1) rotate(0);pointer-events:auto;} +/* badge zoom — 모드 진입 시 bouncy in, 이탈 시 quick out */ +.v5-admin-badge.mode-zoom-in{animation:v5-badge-zoom-in .55s cubic-bezier(.34,1.56,.64,1) both;} +.v5-admin-badge.mode-zoom-out{animation:v5-badge-zoom-out .35s cubic-bezier(.4,0,1,1) both;} +@keyframes v5-badge-zoom-in{ + 0%{opacity:0;transform:scale(0) rotate(-30deg)} + 60%{opacity:1;transform:scale(1.15) rotate(5deg)} + 100%{opacity:1;transform:scale(1) rotate(0)}} +@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)}} @@ -217,12 +226,16 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo .v5-tab:hover .v5-tab-x{opacity:1;} .v5-tab-x:hover{background:rgba(255,71,87,.15);color:var(--v5-red);} -/* Tab collapse */ -.v5-tab-toggle{width:28px;height:28px;border-radius:8px;border:1px solid var(--v5-glass-border); - background:var(--v5-surface);backdrop-filter:blur(8px);color:var(--v5-text-muted);cursor:pointer; - display:flex;align-items:center;justify-content:center;transition:all .25s;flex-shrink:0;margin-right:.35rem;} -.v5-tab-toggle:hover{border-color:var(--v5-primary);color:var(--v5-primary);box-shadow:var(--v5-glow-sm);} -.v5-tab-toggle svg{transition:transform .3s cubic-bezier(.4,0,.2,1);} +/* Tab collapse — 탭 바에 통합된 좌측 핸들 (떠있는 박스 느낌 제거) */ +.v5-tab-toggle{width:32px;height:36px;border:none;background:transparent;color:var(--v5-text-muted);cursor:pointer; + display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:0;font-family:inherit; + border-right:1px solid var(--v5-glass-border);margin-right:.4rem;position:relative; + transition:color .2s,background .2s;} +.v5-tab-toggle::after{content:'';position:absolute;left:4px;right:5px;top:6px;bottom:6px;border-radius:8px; + background:transparent;transition:background .2s;pointer-events:none;} +.v5-tab-toggle:hover{color:var(--v5-primary);} +.v5-tab-toggle:hover::after{background:var(--v5-surface-hover);} +.v5-tab-toggle svg{position:relative;z-index:1;transition:transform .3s cubic-bezier(.4,0,.2,1);} .v5-tabs.collapsed .v5-tab-toggle svg{transform:rotate(180deg);} .v5-tabs.collapsed{height:0;padding:0;border:none;overflow:hidden;transition:height .3s cubic-bezier(.4,0,.2,1),padding .3s,border-width .3s;} .v5-tabs:not(.collapsed){transition:height .3s cubic-bezier(.16,1,.3,1),padding .3s;} @@ -480,6 +493,58 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo .v5-tabs.fade-in{animation:v5-tabsFadeIn .3s cubic-bezier(.16,1,.3,1) both;} @keyframes v5-tabsFadeIn{from{opacity:0}to{opacity:1}} +/* ===== MODE TRANSITION — sidebar items morph (option b) ===== */ +/* 각 .v5-si 가 stagger delay 로 사라졌다가 새 메뉴가 stagger 로 들어옴 */ +.v5-si.mode-morph-out{animation:v5-mode-si-out .35s cubic-bezier(.4,0,1,1) forwards;} +.v5-si.mode-morph-in{animation:v5-mode-si-in .45s cubic-bezier(.16,1,.3,1) backwards;} +@keyframes v5-mode-si-out{ + 0%{opacity:1;transform:translateX(0) scale(1);filter:blur(0)} + 100%{opacity:0;transform:translateX(-30px) scale(.92);filter:blur(4px)}} +@keyframes v5-mode-si-in{ + 0%{opacity:0;transform:translateX(30px) scale(.92);filter:blur(4px)} + 100%{opacity:1;transform:translateX(0) scale(1);filter:blur(0)}} + +/* ===== MODE TRANSITION — header glow line flash (option c) ===== */ +/* 헤더 아래 1px 라인이 mode change 시 굵게/번쩍 → 1px 로 settle */ +.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-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)} + 20%{opacity:1;height:6px;filter:blur(8px)} + 40%{opacity:1;height:4px;filter:blur(6px)} + 100%{opacity:.6;height:1px;filter:blur(0)}} + +/* ===== MODE TRANSITION — breadcrumb text swap (option e) ===== */ +.v5-hdr-bc{display:inline-block;} +.v5-hdr-bc.mode-swap-out{animation:v5-mode-bc-out .25s ease-in forwards;} +.v5-hdr-bc.mode-swap-in{animation:v5-mode-bc-in .35s cubic-bezier(.16,1,.3,1) forwards;} +@keyframes v5-mode-bc-out{ + to{opacity:0;transform:translateY(-8px) scale(.9);filter:blur(4px)}} +@keyframes v5-mode-bc-in{ + 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 (option d, 절제된 버전) ===== */ +/* JS 가 .v5-mode-burst 컨테이너를 body 에 append. ring 1개 + particle 6개 정도로 미니멀. */ +.v5-mode-burst{position:fixed;pointer-events:none;z-index:1000;} +.v5-mode-burst .burst-ring{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%); + width:0;height:0;border-radius:50%;border:1.5px solid var(--v5-primary);opacity:.6; + animation:v5-mode-burst-ring .65s cubic-bezier(.16,1,.3,1) forwards;} +.v5-mode-burst.admin .burst-ring{border-color:var(--v5-cyan);} +@keyframes v5-mode-burst-ring{ + 0%{width:0;height:0;opacity:.8;border-width:1.5px} + 100%{width:140px;height:140px;opacity:0;border-width:.5px}} +.v5-mode-burst .burst-particle{position:absolute;left:50%;top:50%;width:3px;height:3px; + border-radius:50%;background:var(--v5-primary);box-shadow:0 0 4px var(--v5-primary);opacity:.7; + animation:v5-mode-burst-particle .55s cubic-bezier(.16,1,.3,1) forwards;} +.v5-mode-burst.admin .burst-particle{background:var(--v5-cyan);box-shadow:0 0 4px var(--v5-cyan);} +@keyframes v5-mode-burst-particle{ + 0%{transform:translate(-50%,-50%) scale(1);opacity:.8} + 100%{transform:translate(calc(-50% + var(--bx)),calc(-50% + var(--by))) scale(0);opacity:0}} + /* ===== THEME TRANSITION ===== */ .v5-theme-fade{position:fixed;inset:0;z-index:9999;pointer-events:none;opacity:0; transition:opacity .4s cubic-bezier(.4,0,.2,1);} diff --git a/notes/gbpark/2026-04-08-admin-cosmic-full-gallery.html b/notes/gbpark/2026-04-08-admin-cosmic-full-gallery.html index 15d08357..9ef0d495 100644 --- a/notes/gbpark/2026-04-08-admin-cosmic-full-gallery.html +++ b/notes/gbpark/2026-04-08-admin-cosmic-full-gallery.html @@ -167,16 +167,37 @@ html:not(.dark) .v5-cosmos .neb-3{width:800px;height:350px;top:auto;bottom:5%;le /* 사이드바 그룹 (접기 가능) */ .v5-side-grp{display:flex;flex-direction:column;gap:1px;} -.v5-side-grp-hd{display:flex;align-items:center;gap:.5rem;padding:.45rem .65rem;cursor:pointer; - font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--v5-text-muted); - border-radius:8px;transition:all .2s;margin-top:.4rem;} -.v5-side-grp-hd:hover{background:var(--v5-surface-hover);color:var(--v5-text-sec);} -.v5-side-grp-hd .arr{margin-left:auto;width:11px;height:11px;transition:transform .3s;} +.v5-side-grp-hd{display:flex;align-items:center;gap:.5rem;padding:.5rem .65rem;cursor:pointer; + font-size:.72rem;font-weight:700;letter-spacing:-.005em;color:var(--v5-text-sec); + border-radius:9px;transition:all .2s;margin-top:.5rem;} +.v5-side-grp-hd:hover{background:var(--v5-surface-hover);color:var(--v5-text);} +.v5-side-grp-hd .ic-grp{display:flex;align-items:center;justify-content:center;width:14px;height:14px;color:var(--v5-text-muted);} +.v5-side-grp-hd:hover .ic-grp{color:var(--v5-primary);} +.v5-side-grp-hd .arr{margin-left:auto;width:11px;height:11px;transition:transform .3s;color:var(--v5-text-muted);} .v5-side-grp.collapsed .arr{transform:rotate(-90deg);} .v5-side-grp.collapsed .v5-side-grp-bd{max-height:0;opacity:0;overflow:hidden;} .v5-side-grp-bd{max-height:1500px;opacity:1;transition:max-height .35s cubic-bezier(.4,0,.2,1),opacity .25s;display:flex;flex-direction:column;gap:1px;} .v5-side-grp-bd .v5-si{margin-left:.35rem;padding-left:.85rem;} +/* 단독 메뉴 항목 (그룹 헤더 없이) */ +.v5-si.standalone{margin-top:.15rem;} + +/* 사이드바 상단 — 현재 회사 카드 */ +.v5-comp-card{display:flex;align-items:center;gap:.55rem;padding:.65rem .7rem;border-radius:11px; + background:linear-gradient(135deg,rgba(108,92,231,.08),rgba(0,206,201,.04)); + border:1px solid var(--v5-glass-border);margin-bottom:.65rem;cursor:pointer;transition:all .25s;} +.v5-comp-card:hover{border-color:var(--v5-primary);box-shadow:var(--v5-glow-sm);transform:translateY(-1px);} +.dark .v5-comp-card{background:linear-gradient(135deg,rgba(162,155,254,.1),rgba(85,239,196,.04));} +.v5-comp-card .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center; + background:linear-gradient(135deg,var(--v5-primary),var(--v5-cyan));color:#fff;flex-shrink:0;box-shadow:var(--v5-glow-sm);} +.v5-comp-card .info{flex:1;min-width:0;} +.v5-comp-card .lbl{font-size:.5rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--v5-text-muted);} +.v5-comp-card .name{font-size:.74rem;font-weight:700;color:var(--v5-text);margin-top:.1rem;display:flex;align-items:center;gap:.3rem;} +.v5-comp-card .name .role{font-size:.55rem;font-weight:600;color:var(--v5-primary);padding:.05rem .35rem;border-radius:6px;background:rgba(108,92,231,.1);} +.dark .v5-comp-card .name .role{color:var(--v5-primary-light);background:rgba(162,155,254,.12);} +.v5-comp-card .arr{color:var(--v5-text-muted);transition:transform .2s;} +.v5-comp-card:hover .arr{color:var(--v5-primary);transform:translateY(1px);} + /* Content */ .v5-content{flex:1;overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:1rem;} @@ -320,6 +341,57 @@ html:not(.dark) .v5-cosmos .neb-3{width:800px;height:350px;top:auto;bottom:5%;le .v5-pattern{display:none;flex-direction:column;gap:1rem;flex:1;min-height:0;} .v5-pattern.on{display:flex;} +/* ============================================================ + PATTERN: CARD GRID (사용자관리/회사관리/권한 그룹관리) + 좌: 트리/필터 사이드 + 우: KPI + 카드 그리드 + ============================================================ */ +.v5-cg-shell{display:grid;grid-template-columns:240px 1fr;gap:1rem;flex:1;min-height:0;} +.v5-cg-side{display:flex;flex-direction:column;gap:.85rem;overflow-y:auto;} +.v5-cg-main{display:flex;flex-direction:column;gap:.85rem;min-width:0;overflow-y:auto;} + +/* 사이드 패널 (필터/트리) */ +.v5-cg-side .v5-card{flex-shrink:0;} +.v5-cg-side .panel-bd{padding:.6rem .75rem;display:flex;flex-direction:column;gap:.45rem;} +.v5-cg-side .filter-chip{display:flex;align-items:center;gap:.5rem;padding:.42rem .6rem;border-radius:9px; + font-size:.68rem;color:var(--v5-text-sec);cursor:pointer;transition:all .2s;} +.v5-cg-side .filter-chip:hover{background:var(--v5-surface-hover);color:var(--v5-text);transform:translateX(2px);} +.v5-cg-side .filter-chip.on{background:linear-gradient(135deg,rgba(108,92,231,.12),rgba(108,92,231,.04));color:var(--v5-primary);font-weight:600;} +.dark .v5-cg-side .filter-chip.on{background:linear-gradient(135deg,rgba(162,155,254,.14),rgba(162,155,254,.05));color:var(--v5-primary-light);} +.v5-cg-side .filter-chip .ic{width:13px;height:13px;display:flex;align-items:center;justify-content:center;} +.v5-cg-side .filter-chip .cnt{margin-left:auto;font-size:.55rem;font-weight:700;color:var(--v5-text-muted); + background:var(--v5-surface);padding:.05rem .35rem;border-radius:6px;} +.v5-cg-side .filter-chip.on .cnt{color:var(--v5-primary);background:rgba(108,92,231,.1);} +.dark .v5-cg-side .filter-chip.on .cnt{color:var(--v5-primary-light);background:rgba(162,155,254,.12);} + +/* 메인 KPI (가로 6개, 컴팩트, 톤다운) */ +.v5-cg-kpi{display:grid;grid-template-columns:repeat(6,1fr);gap:.5rem;} +.v5-cg-kpi .kpi{position:relative;padding:.55rem .7rem;border-radius:9px; + background:var(--v5-glass);backdrop-filter:blur(16px) saturate(1.2); + border:1px solid var(--v5-glass-border);overflow:hidden;transition:border-color .2s;} +.v5-cg-kpi .kpi:hover{border-color:var(--v5-primary);} +.v5-cg-kpi .kpi::before{content:'';position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--v5-primary);opacity:.7;} +.v5-cg-kpi .kpi.cyan::before{background:var(--v5-cyan);} +.v5-cg-kpi .kpi.pink::before{background:var(--v5-pink);} +.v5-cg-kpi .kpi.amber::before{background:var(--v5-amber);} +.v5-cg-kpi .kpi.green::before{background:var(--v5-green);} +.v5-cg-kpi .kpi .lbl{font-size:.5rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--v5-text-muted);} +.v5-cg-kpi .kpi .val{font-size:1rem;font-weight:700;color:var(--v5-text);margin-top:.1rem;letter-spacing:-.01em;} +.v5-cg-kpi .kpi .delta{font-size:.5rem;color:var(--v5-text-muted);margin-top:.05rem;} +.v5-cg-kpi .kpi .delta.up{color:var(--v5-green);} +.v5-cg-kpi .kpi .delta.down{color:var(--v5-red);} + +/* 리스트 카드 컨테이너 */ +.v5-cg-grid-card{padding:0;overflow:hidden;flex:1;min-height:0;display:flex;flex-direction:column;} + +/* 도구바 */ +.v5-cg-toolbar{display:flex;align-items:center;gap:.5rem;padding:.55rem .85rem;} +.v5-cg-toolbar .grow{flex:1;} + +/* cardgrid 패턴 안의 테이블 행 — 약간 컴팩트 */ +.v5-pattern[data-pattern="cardgrid"] .v5-table tbody td{padding:.55rem .85rem;} +.v5-pattern[data-pattern="cardgrid"] .v5-table tbody td .v5-user-cell .av{width:26px;height:26px;font-size:.55rem;} +.v5-pattern[data-pattern="cardgrid"] .v5-pager{padding:.6rem 1rem;} + /* ============================================================ PATTERN: DASHBOARD ============================================================ */ @@ -518,8 +590,23 @@ html:not(.dark) .v5-cosmos .neb-3{width:800px;height:350px;top:auto;bottom:5%;le
- - + +
@@ -548,81 +635,31 @@ html:not(.dark) .v5-cosmos .neb-3{width:800px;height:350px;top:auto;bottom:5%;le
- - -
-
-
전체 항목
-
128
- ▲ 12 -
-
-
-
활성
-
112
- ▲ 87.5% -
-
-
-
오늘 추가
-
7
- ▲ 3 -
-
-
-
경고
-
3
- ▼ 1 -
-
-
+ +
검색 조건
조건 후 검색
-
-
- - -
- - - - - -
+
-
목록
128건 · 1 / 13
+
목록
총 0건
- - - - - - - - - - - - +
이름 / 코드분류설명담당자상태최종 수정관리
-
페이지당1 - 20 / 128
+
페이지당1 - 20 / 0
@@ -640,6 +677,52 @@ html:not(.dark) .v5-cosmos .neb-3{width:800px;height:350px;top:auto;bottom:5%;le
+ +
+
+ + + + +
+ +
+ + +
+
+
+ + +
+ +
+ + + +
+
+ + + +
+
+
+
페이지당1 - 12 / 12
+
+ + + +
+
+
+
+
+
+ @@ -1229,100 +1312,51 @@ html:not(.dark) .v5-cosmos .neb-3{width:800px;height:350px;top:auto;bottom:5%;le + + diff --git a/notes/gbpark/2026-04-08-invyone-rebuild-spec.md b/notes/gbpark/2026-04-08-invyone-rebuild-spec.md index 5e8073c0..00f7faa9 100644 --- a/notes/gbpark/2026-04-08-invyone-rebuild-spec.md +++ b/notes/gbpark/2026-04-08-invyone-rebuild-spec.md @@ -1,885 +1,340 @@ -# invyone 재빌드 — 시스템 정의서 (초안 v0.1) +# invyone 리팩토링 — 대시보드 + 템플릿 SPEC (v0.2) > **상태**: DRAFT — 사용자 검토 대기 > **작성일**: 2026-04-08 > **작성자**: gbpark + Claude +> **이전 버전**: v0.1 (885줄) — 방향성(메타 모델 3계층, 인증 재활용, LLM 친화 컬럼 정책)은 맞았지만 톤이 "그린필드 / 새 라우트 격리" 같은 재빌드 쪽으로 새서 폐기. v0.2 는 **순수 리팩토링** 으로 다시 씀. +> +> **★ 핵심 입장**: 새 프로젝트 만드는 것 아님. 기존 invyone 코드(`frontend/app/(main)/dashboard/` 가 이미 있음, `DashboardElement` 가 이미 있음, v5 Cosmic 디자인이 이미 있음) 위에 시안의 UX 컨셉을 **얹는 것**. +> +> **v0.1 에서 흡수한 부분** (방향이 맞은 것): +> - Component / Template / Dashboard 3계층 메타 모델 +> - 기존 인증/멀티테넌시/MyBatis 패턴 재활용 +> - LLM 친화 컬럼 정책 (description / tags_json / semantic_keywords) — M3+ +> - 비범위 정의 (BOM / 생산 / 출하 / 모바일 / i18n) +> +> **v0.1 에서 버린 부분** (방향이 틀린 것): +> - "재빌드" 라는 단어 +> - 새 라우트 격리 (`(invyone)/builder`) — 기존 `/dashboard` 가 이미 있는데 별개 셸을 또 만드는 건 그린필드 발상 +> - v5 Cosmic 디자인 폐기 — 사용자가 "디자인은 유지" 명시 +> - Screen Designer 폐기 결정 — M1 범위 아님 +> - "885줄" 의 광범위함 — 첫 마일스톤은 좁아야 함 +> > **입력 자료**: -> - 다운로드 시안 ZIP (`~/다운로드/INVYONE개발.zip`) — `index.html`, `developer.html`, `templates/hr-management.html`, `js/app.js`, `js/template-html.js` -> - 현재 invyone 코드 (`~/invyone/`) — `frontend/`, `backend-spring/`, `ai-assistant/`, `README.md` +> - 다운로드 시안 ZIP (`~/다운로드/INVYONE개발.zip`) — `index.html`, `developer.html`, `templates/hr-management.html`, `js/app.js`, `js/template-html.js` (디자이너 시안, vanilla HTML/CSS/JS — **참고만, 맹신 금지**) +> - 현재 invyone 코드 — `frontend/app/(main)/dashboard/page.tsx`, `frontend/app/(main)/dashboard/[dashboardId]/page.tsx`, `frontend/components/dashboard/DashboardViewer.tsx`, `frontend/components/admin/dashboard/types.ts`, `frontend/lib/api/dashboard.ts`, `frontend/styles/v5-layout.css`, `frontend/invion-layout-v5.html`, `backend-spring/src/main/resources/mapper/auth.xml` +> - **v5 톤 mockup HTML** (이번 SPEC 작업 중 만듦): `notes/gbpark/2026-04-08-invyone-mockup-dashboard.html` — 이게 SPEC 의 시각적 진실의 원천. SPEC 과 모순되면 mockup 이 정답 > - 사용자 인터뷰 (2026-04-08 채팅 세션) --- -## 1. 개요 / 목적 / 범위 +## 0. 한 줄 요약 -### 1.1 무엇을 다시 짓는가 -현재 invyone 은 **데이터타입관리 → 화면디자이너 → 제어관리 → 배치관리** 가 별개의 메뉴/단계로 흩어진 4단계 워크플로우. 사용자가 화면 하나 만들려면 4개 메뉴를 갈아타며 단계별로 정의해야 함. 이걸 **한 캔버스 위에서 한 번에 끝내는 통합 빌더 UX** 로 재구성. +**기존 invyone 의 dashboard 시스템(이미 존재함)에 "Template" 단위 element 를 새로 추가해서, 사용자가 인사정보 같은 큰 화면을 카드로 잡아 캔버스에 배치할 수 있게 만든다. 디자인은 v5 Cosmic 그대로. 첫 마일스톤은 인사정보 템플릿 1개.** -추가로 기존 코드는 AI 가 짬뽕으로 만들어서 **규격이 없음**. 새 프로젝트는: -- **Spec First** — 코드 짜기 전에 메타 모델 / DB 스키마 / 컴포넌트 규격을 문서로 박음 -- **LLM Native DB** — 나중에 로컬 LLM 이 자연어로 워크플로우를 자동 생성할 수 있게 스키마 단계에서 길을 뚫어둠 +> **이건 리팩토링이지 재빌드/그린필드 아님.** `frontend/app/(main)/dashboard/[dashboardId]/page.tsx` 에 이미 DashboardViewer 가 있고, `DashboardElement` 타입에 `position{x,y} + size{w,h}` 가 이미 있고, `lib/api/dashboard.ts` 가 이미 있다. M1 작업의 80% 는 **기존 코드에 type:'template' 분기 한 줄 추가 + 인사정보 React 컴포넌트 1개 신규** 가 전부다. 새 라우트 안 만든다. 새 셸 안 만든다. -### 1.2 본 SPEC 의 범위 -- 빌더 UX 의 메타 모델 (Component / Template / Dashboard 3계층) -- 사용자 모드 / 개발자 모드 분리 정의 -- 라우팅 / 페이지 구조 -- DB 스키마 초안 (LLM-friendly 컬럼 정책 포함) -- 첫 마일스톤 (M1) 의 정확한 정의 -- AI 어시스턴트 통합 hook 의 자리 (실연결은 비범위) +## 0.1 mockup 시각 자료 -### 1.3 비범위 (이번 SPEC 에서 정의 안 함) -- ERP 도메인 (BOM / 생산 / 출하 / 세금계산서 / 물류) 의 비즈니스 로직 — 후속 마일스톤에서 도메인별 별도 SPEC -- 로컬 LLM 모델 선정, 임베딩 모델, 벡터 DB 위치 -- 모바일 전용 UX -- 다국어 (i18n) 정책 — 일단 한국어 단일 -- 기존 invyone 데이터 마이그레이션 (= 비범위, 별개 프로젝트로 취급, 기존 데이터는 테스트 데이터로 활용) +`notes/gbpark/2026-04-08-invyone-mockup-dashboard.html` — 브라우저로 열면 됨. 이 SPEC 의 모든 UX 결정은 이 mockup 으로 시각화되어 있음. 보이는 것: -### 1.4 무엇을 재사용하는가 -| 자산 | 위치 | 용도 | +- v5 Cosmic 셸 (헤더 / 탭 / 사이드바 / 캔버스) — `invion-layout-v5.html` 베이스 +- 사이드바 메뉴: **워크스페이스(인사 대시보드 / 매출 / 재고 / + 새 대시보드) + 도메인(ERP/MES/SCM) + 분석(리포트/AI)** +- 캔버스 위에 인사정보 템플릿 카드 1개 (통계 4개 + 필터 + 사원 테이블) +- 우측에 빈 자리 2개 (다음 카드 추가 위치) +- 우상단 "+ 템플릿 추가" 버튼 → 라이브러리 모달 (인사정보 활성, M2/M4 카드는 dim) +- 헤더의 톱니 버튼 → 사용자 ↔ 개발자 모드 토글 (시안의 mode-switch 와 같은 컨셉, 기존 v5 의 admin-mode 를 재사용) + +이 mockup 이 SPEC 의 시각적 정답. SPEC 텍스트와 mockup 이 어긋나면 mockup 이 맞음. + +--- + +## 1. 목적 / 방향 + +### 1.1 무엇을 하는가 +- 기존 invyone 코드의 **리팩토링**. 새 프로젝트 X. 새 라우트 격리 X. +- 시안의 핵심 컨셉(= 사용자가 캔버스에 큰 템플릿 카드를 배치) 을 기존 dashboard 시스템 위에 얹는다. +- 기존 v5 Cosmic 디자인 그대로 사용. 시안의 `#4a6cf7` 파란/흰 톤은 가져오지 않는다. + +### 1.2 무엇을 하지 않는가 +- v5 Cosmic 디자인 폐기 X — `frontend/styles/v5-layout.css` 토큰 그대로 유지 +- 시안의 80개 컴포넌트 라이브러리 통째 이식 X +- 시안의 iframe + srcdoc 렌더링 방식 채택 X — React 컴포넌트로 구현 +- 시안의 옵션 패널(네비위치/테마컬러 등 화이트라벨링) 첫 마일스톤 범위 아님 +- 사용자 모드 / 개발자 모드 분리 첫 마일스톤 범위 아님 (M2 이후) +- 기존 Screen Designer (`frontend/components/screen/` 147 파일) 폐기 X — 안 건드림 + +### 1.3 시안 (`INVYONE개발.zip`) 의 위치 +- **참고 자료**. 맹신 금지. +- 가져올 것: 사용자가 캔버스 위에 큰 화면 카드를 배치하는 UX 흐름, 인사관리 화면의 정보 구조(통계카드 + 필터 + 테이블 + 상세) +- 버릴 것: 디자인 톤(파란/흰), iframe srcdoc, 컴포넌트 80개 카탈로그, 옵션 패널 세부사항 + +--- + +## 2. 기존 자산 인벤토리 (★ 리팩토링 SPEC 의 출발점) + +### 2.1 이미 있는 것 — 그대로 활용 + +| 자산 | 위치 | 역할 | |---|---|---| -| 인증 / JWT | `backend-spring/.../security/`, `frontend/app/(auth)/login/` | 그대로 | -| 회사 / 사용자 / 부서 / 권한 | `USER_INFO`, `COMPANY_INFO`, `DEPARTMENT`, `ROLE_INFO` 테이블 + 관련 컨트롤러 | 그대로 | -| 멀티테넌시 (`company_code` 필터) | `JwtAuthenticationFilter`, `common.companyCodeFilter` XML include | 그대로 | -| AI 어시스턴트 API | `ai-assistant/` (포트 3100, Gemini 기반) | 자리만 두고, 나중에 로컬 LLM 어댑터로 교체 | -| MyBatis Map-based 패턴 | `BaseService` + XML mapper | 새 도메인 만들 때 동일 패턴 사용 | -| Next.js 15 + React 19 + Tailwind v4 + shadcn/ui | `frontend/` 인프라 | 그대로 | +| Dashboard 목록 페이지 | `frontend/app/(main)/dashboard/page.tsx` | 저장된 dashboard 카드 그리드. 검색, 새로 만들기 버튼 | +| Dashboard 뷰어 페이지 | `frontend/app/(main)/dashboard/[dashboardId]/page.tsx` | 특정 dashboard 를 읽기 전용으로 표시 | +| DashboardViewer 컴포넌트 | `frontend/components/dashboard/DashboardViewer.tsx` | elements 배열을 받아 절대 위치로 렌더링 | +| DashboardElement 타입 | `frontend/components/admin/dashboard/types.ts` | `{ id, type, position{x,y}, size{w,h}, ... }` | +| Dashboard API 클라이언트 | `frontend/lib/api/dashboard.ts` | `getDashboards()`, `getDashboard(id)` | +| 인증 / 멀티테넌시 | `backend-spring/.../security/`, `JwtAuthenticationFilter` | `company_code` 필터 그대로 | +| USER_INFO / DEPARTMENT / ROLE_INFO | DB + `auth.xml`, `department.xml`, `role.xml` 매퍼 | 인사정보 화면의 데이터 소스 | +| v5 Cosmic 디자인 토큰 | `frontend/styles/v5-layout.css` | `--v5-primary`, `--v5-glass`, `.v5-card`, `.v5-btn` 등 | +| MyBatis Map-based 패턴 | `BaseService` + XML mapper | 새 도메인은 동일 패턴 | -### 1.5 무엇을 폐기하는가 -- v5 Cosmic Glassmorphism 디자인 (`frontend/styles/v5-layout.css`, `invion-preview-v5.html`) — 새 빌더에서는 안 씀. 기존 화면은 유지. -- 기존 4단계 워크플로우의 메뉴 구조 — 새 빌더 라우트는 기존과 격리 -- (잠재적으로) `frontend/components/screen/` 의 Screen Designer 147 파일 — **새 빌더가 안정화되면** 점진적 폐기. 첫 마일스톤에선 건드리지 않음. +### 2.2 없는 것 — 새로 만들어야 함 ---- - -## 2. 용어 정의 - -| 용어 | 정의 | -|---|---| -| **Component** | 빌더의 원자 단위. 차트/테이블/폼/카드/컨테이너 등. 재사용 가능한 React 컴포넌트 + 메타데이터(id, 이름, 카테고리, 태그, 컨테이너 여부, 기본 props) | -| **Container Component** | 다른 Component 를 자식으로 받을 수 있는 Component. grid-layout / tab-panel / accordion / card-layout / split-panel. 자식 중첩은 1단계 (자식의 자식 X). | -| **Template** | Component 들을 조립한 완성품. 개발자 모드에서 만듦. 회사 단위 또는 시스템 기본 제공. (예: "인사 관리", "매출 대시보드") | -| **Dashboard** | 사용자가 자기 작업 공간에 만든 캔버스. 여러 Template 을 자유 배치 (absolute position, 드래그/리사이즈). 사용자 1명이 N 개 보유 가능. | -| **Dashboard Item** | Dashboard 안에 배치된 한 Template 인스턴스. 위치/크기/접힘여부/카드스타일을 가짐. | -| **사용자 모드** | Template 라이브러리에서 골라 Dashboard 에 배치 + 데이터 입력/조회. 컴포넌트 조립은 못 함. | -| **개발자 모드** | Component 를 조립해 새 Template 을 만들 수 있음. 권한이 있어야 진입. | -| **회사 화이트라벨링** | 회사별로 네비위치/테마컬러/폰트/배경/네비바색을 다르게 설정. (시안의 옵션 패널 그대로) | -| **AI 어시스턴트** | 자연어 입력으로 Template 자동 생성, 데이터 입력, 워크플로우 자동화. 첫 마일스톤은 자리만, 실연결은 후속. | - ---- - -## 3. 시스템 아키텍처 - -### 3.1 컴포넌트 다이어그램 (텍스트) - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Browser (Next.js Client) │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ -│ │ (auth)/login │ │ (main)/... │ │ (invyone)/builder │ │ -│ │ [기존 그대로] │ │ [기존 INVION]│ │ [NEW] 통합 빌더 라우트│ │ -│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ -│ │ │ │ │ -│ └────────────────┴────────────────────┘ │ -│ │ │ -│ /api 프록시 │ -└────────────────────────────┼─────────────────────────────────────┘ - │ - ┌──────────────┴──────────────┐ - │ │ - ┌──────────▼──────────┐ ┌──────────▼──────────┐ - │ backend-spring 8081 │ │ ai-assistant 3100 │ - │ ┌─────────────────┐ │ │ (Gemini → 로컬 LLM) │ - │ │ 기존 컨트롤러 95│ │ │ /api/ai/v1/* │ - │ │ + NEW 빌더 도메인│ │ └─────────────────────┘ - │ │ - components │ │ ▲ - │ │ - templates │ │ │ (M5 에서 연결) - │ │ - dashboards │ │ │ - │ │ - theme │ │ │ - │ └─────────────────┘ │ │ - │ │ │ │ - │ PostgreSQL │ ◄────────────────┘ - │ (testvex / 신규 sch)│ (LLM 이 메타 + 데이터 읽음) - └─────────────────────┘ -``` - -### 3.2 데이터 흐름 (사용자 모드 — 첫 마일스톤 기준) - -``` -1. 로그인 (기존 JWT) → company_code 추출 -2. /invyone/builder 진입 → Dashboard 목록 GET -3. Dashboard 선택 → Dashboard 의 items[] (Template 인스턴스들) GET -4. 각 Item 의 templateId 로 Template 메타 + 컴포넌트 트리 GET -5. 클라이언트가 트리를 React 로 렌더 (iframe 격리 X) -6. 편집 모드 → 위치/크기 변경 → PATCH dashboard items -7. 새 Template 추가 → 라이브러리 모달 → 선택 → POST item -``` - -### 3.3 데이터 흐름 (개발자 모드 — M3) - -``` -1. 권한 확인 (user_type 이 dev 가능 권한일 때만 토글 활성) -2. /invyone/builder/dev 진입 -3. 좌측: Components GET (시스템 + 회사 커스텀) -4. 캔버스: 빌더 상태 (메모리) -5. 우측 속성패널: 선택 컴포넌트의 props 편집 -6. 저장 → POST template (회사 단위 저장) -7. 사용자 모드 라이브러리에 즉시 노출 -``` - -### 3.4 데이터 흐름 (AI 어시스턴트 — M5, hook 만 M1 에 박음) - -``` -1. 사용자: "수주 만들어줘" (자연어 입력) -2. /api/ai/v1/builder/generate POST { prompt, context: { dashboardId, companyCode } } -3. ai-assistant 가 components / templates 메타 + 회사 데이터 스키마 읽음 -4. LLM 이 Template 트리 JSON 생성 + 필요시 신규 DB 테이블 제안 -5. 응답: { template: {...}, suggestions: [...], questions: [...] } -6. 클라이언트가 사용자에게 확인 모달 → 승인 시 POST template + POST item -7. 후속 단계: 사용자가 답변한 데이터로 필드 채움 → 자동 발주 트리거 (워크플로우 엔진) -``` - ---- - -## 4. 도메인 / 메타 모델 (3계층) - -### 4.1 계층 관계 - -``` -Component (원자, 시스템 + 회사 커스텀) - │ - │ 여러 개 조립 - ▼ -Template (조립된 mini-app, 회사 단위) - │ - │ N개 배치 - ▼ -Dashboard (사용자의 작업 캔버스, 사용자 단위) - │ - │ 자유 배치 (위치/크기) - ▼ -Dashboard Item (Template 인스턴스 + 위치 메타) -``` - -### 4.2 Component 메타 모델 - -```typescript -interface Component { - // 식별 - componentId: string; // 'kpi-card', 'data-table', ... - type: string; // 동일 (호환용) - companyCode?: string; // null = 시스템 기본, 값 = 회사 커스텀 - - // 표시 - nameKo: string; // '바 차트' - nameEn?: string; // 'Bar Chart' (i18n 대비) - description: string; // '수직/수평 막대 그래프' - icon: string; // emoji 또는 lucide 아이콘 키 - category: string[]; // ['layout', 'chart', ...] (10 카테고리) - tags: string[]; // ['ERP', 'MES', '대시보드', ...] - - // 구조 - isContainer: boolean; - defaultCols?: number; // 컨테이너 전용 - allowedChildTypes?: string[]; // 컨테이너 전용. null 이면 전체 허용 - maxNestingDepth: 1; // 자식의 자식 금지 (시안 동일) - - // 기본 속성 - defaultProps: { - width: 'full' | 'half' | 'third' | 'quarter' | 'two-third'; - height: 'auto' | 'small' | 'medium' | 'large' | 'xlarge'; - bgColor: string; // hex - border: 'solid' | 'dashed' | 'none'; - }; - - // 렌더링 - reactComponentKey: string; // 프론트 레지스트리에서 lookup 할 키 - // (예: 'KpiCard' → registry.get('KpiCard')) - - // LLM 친화 슬롯 (M5 hook) - semanticDescription?: string; // "이 컴포넌트는 핵심 지표 한 개를 큰 숫자로 표시한다" - exampleUseCases?: string[]; // ["월매출 표시", "재고 수량 표시"] - embeddingVector?: number[]; // 512차원 등, M5 에서 채움 (지금은 nullable) - - // 메타 - isActive: boolean; - sortOrder: number; - createdAt: Date; - updatedAt: Date; -} -``` - -### 4.3 Template 메타 모델 - -```typescript -interface Template { - // 식별 - templateId: string; // 'tpl-hr-mgmt' 또는 'custom-{ts}' - companyCode: string; // 시스템 기본은 'SYSTEM' 회사 - isSystem: boolean; // 시스템 기본 제공 여부 - isCustom: boolean; // 사용자가 만든 커스텀 여부 - - // 표시 - nameKo: string; - description: string; - icon: string; - category: string[]; // ['dashboard', 'erp', 'mes', ...] (Template 카테고리, Component 와 다름) - tags: string[]; - - // 구조 (트리) - layout: TemplateNode[]; // root 레벨 컴포넌트들 - - // LLM 친화 슬롯 - semanticDescription?: string; // "사원 목록과 부서별 통계, 검색/필터를 한 화면에 제공" - domainKeywords?: string[]; // ["인사", "사원", "부서", "급여"] - embeddingVector?: number[]; - - // 권한 - visibilityScope: 'company' | 'department' | 'private'; - allowedRoleIds?: string[]; - - // 메타 - createdBy: string; // user_id - createdAt: Date; - updatedAt: Date; - version: number; // 변경 시 +1 -} - -interface TemplateNode { - componentId: string; // Component.componentId 참조 - displayName: string; // 사용자가 바꾼 이름 - props: { - width: string; - height: string; - bgColor: string; - border: string; - cols?: number; // container 전용 - gap?: number; // container 전용 - }; - children?: TemplateNode[]; // container 전용 (1단계 중첩) - - // LLM 친화 슬롯 - semanticHint?: string; // "이 영역은 부서별 사원 수를 보여준다" - dataBinding?: { // M2~ 에서 채움 - table?: string; - columns?: string[]; - filter?: string; - }; -} -``` - -### 4.4 Dashboard 메타 모델 - -```typescript -interface Dashboard { - dashboardId: string; // UUID - companyCode: string; - ownerId: string; // user_id - - nameKo: string; - isDefault: boolean; // 사용자별 기본 1개 - sortOrder: number; // 사이드바/탭 순서 - - items: DashboardItem[]; - - createdAt: Date; - updatedAt: Date; -} - -interface DashboardItem { - itemId: string; - templateId: string; // Template 참조 - displayName?: string; // 인스턴스 이름 (override) - - // 위치/크기 (시안의 _xp/_wp/_y/_h 그대로 채택) - xRatio: number; // 0~1, 컨테이너 폭 대비 - widthRatio: number; // 0~1 - yPx: number; // 절대 픽셀 - heightPx: number; // 절대 픽셀 - - // 접힘/펼침 상태 (시안 동일) - isCollapsed: boolean; - collapsedX?: number; - collapsedY?: number; - collapsedW?: number; - collapsedH?: number; - cardStyle?: 'flat' | 'glass' | 'neumorphism' | 'gradient' | 'outline' - | 'round' | 'sharp' | 'colortop' | 'sideaccent' | 'solid' - | 'dark' | 'minimal' | 'badge' | 'tile'; - cardBgImage?: string; - - zIndex: number; - - // M5 — AI 가 생성한 인스턴스 표시 - generatedByAi?: boolean; - generatedFromPrompt?: string; -} -``` - -### 4.5 메타 모델 핵심 결정 사항 - -1. **Template 은 진짜 React 컴포넌트 트리** — 시안의 iframe srcdoc 패턴 폐기. 각 컴포넌트가 React 컴포넌트로 직접 렌더되며, 부모 캔버스의 스타일/이벤트와 자연스럽게 통합된다. -2. **자식 중첩은 1단계만** — 시안과 동일. UI 복잡도와 LLM 추론 난이도 둘 다 잡음. -3. **메타데이터에 LLM 친화 슬롯이 1급 시민** — `nameKo`, `description`, `semanticDescription`, `domainKeywords`, `embeddingVector`. 첫 마일스톤에선 채우지 않아도 컬럼은 존재. -4. **시스템 기본 / 회사 커스텀 분리** — Component / Template 둘 다 `companyCode == 'SYSTEM'` 이면 시스템 기본. 회사가 만든 커스텀은 자기 `companyCode` 로만 보임. -5. **Dashboard 는 사용자 단위, Template 은 회사 단위** — 한 회사 안에서 모든 사용자가 같은 Template 라이브러리를 공유. Dashboard 는 개인 작업 공간. - ---- - -## 5. 라우팅 / 페이지 구조 - -### 5.1 새 라우트 그룹 — `(invyone)` - -기존 `(main)/(auth)/(admin)/(pop)` 와 격리된 새 라우트 그룹. 자체 layout. 기존 INVION 메뉴 영향 0. - -``` -frontend/app/ - (invyone)/ - layout.tsx # 시안의 헤더 + 사이드바 (기존 v5 layout 안 씀) - page.tsx # 자동 → /invyone/dashboard/{defaultDashboardId} - dashboard/ - page.tsx # 빈 상태 (대시보드 없을 때) - [dashboardId]/ - page.tsx # 사용자 모드: 캔버스 + 템플릿 배치 - builder/ - page.tsx # 개발자 모드: 컴포넌트 조립 - settings/ - profile/page.tsx # 시안의 프로필 패널 → 페이지화 - company/page.tsx # 시안의 회사 정보 패널 → 페이지화 - theme/page.tsx # 시안의 옵션 패널 → 페이지화 (회사 화이트라벨링) -``` - -### 5.2 시안 화면 ↔ 라우트 매핑 - -| 시안 (HTML) | 새 라우트 | 비고 | +| 자산 | 만들 위치 | 역할 | |---|---|---| -| `index.html` 기본 화면 | `/invyone/dashboard/[id]` | 사용자 모드 캔버스 | -| `index.html` 빈 상태 | `/invyone/dashboard` | 대시보드 0개일 때 | -| `index.html` 템플릿 라이브러리 모달 | `/invyone/dashboard/[id]` 의 모달 컴포넌트 | 라우트 분리 X | -| `index.html` 사이드바 (대시보드 관리) | layout 의 슬라이드 사이드바 | 라우트 분리 X | -| `index.html` 회사 정보 패널 | `/invyone/settings/company` | 패널 → 페이지 | -| `index.html` 내 프로필 패널 | `/invyone/settings/profile` | 패널 → 페이지 | -| `index.html` 옵션 설정 패널 | `/invyone/settings/theme` | 패널 → 페이지 (회사 화이트라벨링) | -| `developer.html` | `/invyone/builder` | 개발자 모드. 별도 창 X, 같은 라우트 | -| `templates/hr-management.html` | (M2~) Template 1개의 React 구현 | 시안의 mini-app → React 컴포넌트 트리 | +| Template 메타 (이름/태그/카테고리/렌더러 키) | `frontend/lib/templates/registry.ts` | 사용 가능한 Template 목록을 정의하는 중앙 레지스트리 | +| Template 렌더러 컨벤션 | `frontend/components/templates/` | 각 Template 은 React 컴포넌트 1개. 위치/크기는 부모(DashboardItem) 가 줌 | +| 인사정보 Template (`hr-employee-list`) | `frontend/components/templates/HrEmployeeList.tsx` | 첫 마일스톤의 유일한 Template. v5 토큰으로 통계카드 + 필터 + 테이블 + 상세 | +| Template 라이브러리 모달 | `frontend/components/dashboard/TemplateLibraryModal.tsx` | 사용자가 캔버스에 새 카드를 추가할 때 뜨는 모달 | +| 인사정보 백엔드 API | `backend-spring/.../controller/HrEmployeeController.java` | `GET /api/hr/employees?page&size&search` 기존 USER_INFO/DEPARTMENT/ROLE_INFO 조인 | +| HR mapper | `backend-spring/src/main/resources/mapper/hrEmployee.xml` | 인사정보 조회 SQL | -### 5.3 layout 설계 +### 2.3 다른 것 — 갭 분석 -`(invyone)/layout.tsx` 의 구조 (시안 그대로): - -``` -┌────────────────────────────────────────────────────────┐ -│ Header (50px) │ -│ [☰] [logo invy.one] [tab nav] [모드토글][...][프로필]│ -├────┬───────────────────────────────────────────────────┤ -│ S │ │ -│ i │ │ -│ d │ │ -│ e │ (dashboard | builder | settings) │ -│ b │ │ -│ a │ │ -│ r │ │ -│ ▼ │ │ -└────┴───────────────────────────────────────────────────┘ -``` - -- 사이드바는 햄버거로 열림/닫힘 (overlay 형태, 시안 그대로) -- 회사 화이트라벨링 설정에 따라 사이드바 위치 (top/left/right/bottom) 변경 -- 헤더의 모드 토글 = 사용자/개발자 라우트 전환 - -### 5.4 미들웨어 - -`frontend/middleware.ts` 에 새 매처 추가: -```ts -'/invyone/:path*' → JWT 검증 (기존 로직 재사용) -'/invyone/builder' → user_type 권한 추가 검증 (개발자 모드) -``` - ---- - -## 6. 빌더 UX 상태머신 - -### 6.1 사용자 모드 (M1 핵심) - -``` -state: { dashboards, activeDashboardId, isEditMode } - -전환: - [INIT] - └→ loadDashboards() → [DASHBOARD_VIEW] - - [DASHBOARD_VIEW] (편집 모드 OFF) - ├→ click(템플릿 추가) → [TEMPLATE_LIBRARY_OPEN] - ├→ click(편집) → [EDIT_MODE] - ├→ click(다른 대시보드) → loadItems() → [DASHBOARD_VIEW] - └→ click(접기/펴기/전체화면) → 인플레이스 상태 변경 - - [TEMPLATE_LIBRARY_OPEN] (모달) - ├→ filter(category|tag|search) → 결과 갱신 - ├→ click(템플릿 카드) → addItem() → POST item → [DASHBOARD_VIEW] - └→ close → [DASHBOARD_VIEW] - - [EDIT_MODE] - ├→ drag(item header) → 위치 변경 (xRatio, yPx) - ├→ resize(handle) → 크기 변경 (widthRatio, heightPx) - ├→ click(추가) → [TEMPLATE_LIBRARY_OPEN] - ├→ click(저장) → PATCH dashboard items → [DASHBOARD_VIEW] - └→ click(삭제) → DELETE item → 즉시 반영 -``` - -### 6.2 개발자 모드 (M3) - -``` -state: { components, currentTemplate, selectedPath, filter } - -전환: - [INIT] - └→ loadComponents() → [BUILDER_EMPTY] - - [BUILDER_EMPTY] - └→ click(컴포넌트) → addComponent() → [BUILDER_EDITING] - - [BUILDER_EDITING] - ├→ click(컴포넌트) → addComponent() - │ - 선택된 게 컨테이너면 자식으로 추가 - │ - 아니면 root 에 추가 - ├→ click(캔버스 카드) → select(path) → [BUILDER_SELECTED] - ├→ click(빈 캔버스) → deselect → [BUILDER_EDITING] - └→ click(템플릿 저장) → 이름/태그 검증 → POST template → [BUILDER_EMPTY] - - [BUILDER_SELECTED] - ├→ 속성 변경 (displayName, width, height, bgColor, border, cols, gap) - ├→ 순서 이동 (▲▼) - ├→ 삭제 → splice → [BUILDER_EDITING] - └→ 다른 카드 클릭 → select(다른 path) → [BUILDER_SELECTED] -``` - -### 6.3 모드 토글 - -- 헤더 우측에 `[🖥️ 사용자] [🛠️ 개발자]` 토글 -- 사용자 → 개발자 클릭: 권한 확인 → `/invyone/builder` 로 navigate -- 개발자 → 사용자 클릭: 미저장 변경 있으면 confirm → `/invyone/dashboard/{lastId}` 로 navigate -- 시안에서는 별도 popup window (`window.open('developer.html')`) 였지만, **새 프로젝트에선 같은 라우트 트리 안에서 navigate**. popup 은 UX 단절 초래. - ---- - -## 7. 컴포넌트 카탈로그 - -### 7.1 카테고리 (10개, 시안 그대로) - -| ID | 이름 | 아이콘 | -|---|---|---| -| `layout` | 레이아웃 | 🧩 | -| `data` | 데이터 | 🗄️ | -| `form` | 입력/폼 | ✏️ | -| `chart` | 차트 | 📊 | -| `nav` | 네비게이션 | 🧭 | -| `media` | 미디어 | 🖼️ | -| `commerce` | 커머스 | 🛒 | -| `social` | 커뮤니티 | 💬 | -| `system` | 시스템 | ⚙️ | - -### 7.2 컴포넌트 목록 (시안 80+ 그대로) - -전체 목록은 `~/다운로드/INVYONE개발/js/app.js:24~110` 의 `ComponentLibrary.components` 배열을 1:1 로 옮긴다. 카테고리별 분포: - -- **layout** (10): header, sidebar, footer, grid-layout, tab-panel, accordion, card-layout, split-panel, modal-popup, wizard -- **data** (10): data-table, tree-table, pivot-table, kanban, timeline, gantt, calendar, tree-view, list-view, master-detail -- **form** (10): input-form, search-filter, file-upload, rich-editor, date-picker, dropdown-select, code-editor, signature-pad, barcode-scanner, address-input -- **chart** (12): bar-chart, line-chart, pie-chart, area-chart, radar-chart, gauge, heatmap, kpi-card, map-chart, scatter-chart, funnel-chart, waterfall-chart -- **nav** (5): top-menu, breadcrumb, pagination, step-nav, mega-menu -- **media** (5): image-gallery, video-player, carousel, file-viewer, icon-set -- **commerce** (8): product-card, cart, checkout, order-list, product-detail, coupon, review, wishlist -- **social** (6): board, comment, chat, notification, user-profile, faq -- **system** (10): login, user-mgmt, role-permission, settings, audit-log, import-export, print-template, workflow, email-template, dashboard-widget - -총 76개 (M1 시점). 이후 회사가 커스텀 컴포넌트를 추가할 수 있음. - -### 7.3 컨테이너 종류와 자식 허용 규칙 - -| 컨테이너 | defaultCols | 자식 허용 | -|---|---|---| -| `grid-layout` | 2 | 모든 비컨테이너 | -| `tab-panel` | 1 | 모든 비컨테이너 (탭별 1개씩) | -| `accordion` | 1 | 모든 비컨테이너 | -| `card-layout` | 3 | 모든 비컨테이너 | -| `split-panel` | 2 | 모든 비컨테이너 | - -**규칙: 자식의 자식 금지** (`maxNestingDepth: 1`). - -### 7.4 M1 에서 실제 렌더되는 컴포넌트 수 - -첫 마일스톤은 **모든 80개를 placeholder 카드로 렌더**한다. 즉: -- 모든 컴포넌트가 라이브러리에 노출됨 -- 캔버스에 떨어뜨리면 "아이콘 + 이름 + 설명 + 'placeholder'" 카드가 렌더됨 -- 실제 동작 (차트 그리기, 테이블 데이터 표시 등) 은 X -- M2 이후 컴포넌트별로 진짜 React 구현 우선순위 정해서 차례로 구현 - ---- - -## 8. 사용자 / 개발자 모드 권한 모델 - -### 8.1 권한 원천 - -기존 invyone 의 `USER_INFO.user_type` 컬럼 활용. JWT 토큰의 claims 에 이미 포함됨 (`backend-spring/.../security/JwtTokenProvider.java:32~48`). - -### 8.2 권한 정의 (M1 에서 시작 상태) - -| user_type | 사용자 모드 | 개발자 모드 | 회사 설정 | 시스템 관리 | -|---|---|---|---|---| -| `SUPER` | ✅ | ✅ | ✅ | ✅ | -| `ADMIN` | ✅ | ✅ | ✅ | ❌ | -| `DEV` | ✅ | ✅ | ❌ | ❌ | -| `USER` | ✅ | ❌ | ❌ | ❌ | - -- 메뉴 / 모드 토글 / 라우트 가드 모두 위 매트릭스 따름 -- M1 에서는 admin/1234 단일 계정으로 SUPER 시작 (시안의 데모 인증과 동일 효과) - -### 8.3 라우트 가드 - -| 라우트 | 최소 권한 | -|---|---| -| `/invyone/dashboard/*` | USER | -| `/invyone/builder` | DEV | -| `/invyone/settings/profile` | USER | -| `/invyone/settings/company` | ADMIN | -| `/invyone/settings/theme` | ADMIN | - -미들웨어에서 JWT claims 의 `user_type` 검증. - ---- - -## 9. 인증 / 회사 / 사용자 통합 방식 - -### 9.1 재사용 원칙 - -- 로그인 화면, JWT 발급, refresh 토큰, 비밀번호 정책 등은 **기존 invyone 그대로** -- 새 빌더는 단지 JWT claims 를 읽어서 `company_code`, `user_id`, `user_type` 사용 - -### 9.2 데이터 격리 - -새로 만드는 모든 빌더 테이블에 `company_code` 컬럼 필수 + 인덱스. 모든 SELECT 에 `WHERE company_code = #{companyCode}` 자동 적용 (기존 `` 패턴 그대로 재사용). - -예외: `companyCode = 'SYSTEM'` 인 행은 모든 회사가 read 가능 (시스템 기본 Component / Template). write 는 SUPER 만. - -### 9.3 사용자 ↔ 회사 정보의 용도 - -| 컨텍스트 | 사용 컬럼 | -|---|---| -| Dashboard 소유자 | `USER_INFO.user_id` → `Dashboard.ownerId` | -| Template 가시성 | `USER_INFO.dept_id`, `USER_INFO.role_id` → `Template.visibilityScope`, `allowedRoleIds` | -| 회사 화이트라벨링 | `COMPANY_INFO.company_code` → `CompanyTheme.companyCode` | -| 멀티테넌시 격리 | 모든 빌더 테이블 `company_code` | - ---- - -## 10. AI 어시스턴트 통합 hook (자리만, 실연결 X) - -### 10.1 진입점 정의 (M1 에 박을 stub) - -빌더 헤더에 자연어 입력창 또는 챗 아이콘. 클릭 시 모달 또는 슬라이드 패널. - -``` -POST /api/ai/v1/builder/intent -Body: { - prompt: "수주 만들어줘", - context: { - companyCode: "ABC123", - userId: "u001", - currentDashboardId: "d042" - } -} -Response (M1 — stub): { - status: "not_implemented", - message: "AI 어시스턴트는 M5 에서 활성화됩니다" -} -``` - -엔드포인트 자체는 M1 에 만들어둠. ai-assistant 가 응답하지 않더라도 frontend 가 안전하게 처리. - -### 10.2 메타 모델의 LLM 친화 슬롯 (M1 에 컬럼만, 값 채우기는 M5) - -다음 컬럼을 처음부터 스키마에 박아둠. 값은 비워둬도 됨: - -- `Component.semanticDescription` — "이 컴포넌트는 ~를 한다" -- `Component.exampleUseCases` — JSON 배열 -- `Component.embeddingVector` — pgvector `vector(512)` 또는 `vector(768)` (모델 결정 시 차원 확정) -- `Template.semanticDescription` -- `Template.domainKeywords` — JSON 배열 -- `Template.embeddingVector` -- `TemplateNode.semanticHint` -- `TemplateNode.dataBinding` — JSON - -### 10.3 미래 시나리오 (M5 ~) - -> "수주 만들어줘" -> 1. /api/ai/v1/builder/intent → ai-assistant -> 2. ai-assistant 가 components / templates 의 semanticDescription + embeddingVector 검색 → 후보 추출 -> 3. 회사 데이터 스키마 (testvex DB 또는 신규 schema) 의 테이블 카탈로그 검색 -> 4. LLM 이 Template JSON 생성 + 신규 테이블 마이그레이션 제안 -> 5. 클라이언트가 사용자에게 `[수정] [그대로 적용]` 모달 -> 6. 적용 → POST template + POST item + (선택) ALTER TABLE -> 7. 사용자에게 알림: "수주 화면이 생성됐어요. 첫 수주를 입력하시겠어요?" -> 8. 사용자 답변 → 필드 자동 채움 → 후속 워크플로우 (자동 발주) 트리거 - -이 시나리오의 끝단까지 모두 첫날부터 SPEC 에 박혀 있어야 메타 모델이 안 흔들림. 그래서 LLM-friendly 컬럼이 1급 시민. - ---- - -## 11. DB 스키마 초안 - -### 11.1 대상 DB 와 스키마 분리 - -- 기존 invyone 데이터: 그대로 (testvex DB) -- 새 빌더 테이블: **별도 PostgreSQL schema** `invyone_builder` 로 격리. 같은 DB 안에 있되 `invyone_builder.components`, `invyone_builder.templates` 등의 풀 네임으로 접근. -- 이유: 기존 테이블 (수백 개) 과 네임스페이스 충돌 방지 + 통째로 drop / migrate 쉬움 + 권한 분리 가능 - -### 11.2 LLM-friendly 컬럼 정책 - -모든 새 테이블에 다음 패턴: - -- 한글 이름: `name_ko VARCHAR(255) NOT NULL` -- 영문 이름: `name_en VARCHAR(255)` (선택) -- 의미 설명: `semantic_description TEXT` -- 키워드: `keywords JSONB DEFAULT '[]'::jsonb` -- 임베딩: `embedding VECTOR(768)` (차원은 모델 결정 시 확정. M5 까지 nullable) - -테이블/컬럼명 자체는 영문 snake_case (DB 컨벤션 유지), **한글 의미는 컬럼 값**으로. - -### 11.3 신규 테이블 (M1 시작) - -```sql --- pgvector 확장 (없으면 추가) -CREATE EXTENSION IF NOT EXISTS vector; - --- ============================================================ --- 1. components — 컴포넌트 카탈로그 --- ============================================================ -CREATE SCHEMA IF NOT EXISTS invyone_builder; - -CREATE TABLE invyone_builder.components ( - component_id VARCHAR(64) PRIMARY KEY, - company_code VARCHAR(64) NOT NULL, -- 'SYSTEM' = 기본 - type VARCHAR(64) NOT NULL, - name_ko VARCHAR(255) NOT NULL, - name_en VARCHAR(255), - description TEXT, - icon VARCHAR(64), - category JSONB NOT NULL DEFAULT '[]'::jsonb, - tags JSONB NOT NULL DEFAULT '[]'::jsonb, - is_container BOOLEAN NOT NULL DEFAULT FALSE, - default_cols INT, - allowed_child_types JSONB, - default_props JSONB NOT NULL DEFAULT '{}'::jsonb, - react_component_key VARCHAR(128) NOT NULL, - semantic_description TEXT, - example_use_cases JSONB DEFAULT '[]'::jsonb, - embedding VECTOR(768), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - sort_order INT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -CREATE INDEX idx_components_company ON invyone_builder.components(company_code); -CREATE INDEX idx_components_category ON invyone_builder.components USING GIN(category); --- 임베딩 검색용 (M5) --- CREATE INDEX idx_components_embedding ON invyone_builder.components USING ivfflat (embedding vector_cosine_ops); - --- ============================================================ --- 2. templates — 템플릿 (Component 트리) --- ============================================================ -CREATE TABLE invyone_builder.templates ( - template_id VARCHAR(64) PRIMARY KEY, - company_code VARCHAR(64) NOT NULL, - is_system BOOLEAN NOT NULL DEFAULT FALSE, - is_custom BOOLEAN NOT NULL DEFAULT TRUE, - name_ko VARCHAR(255) NOT NULL, - description TEXT, - icon VARCHAR(64), - category JSONB NOT NULL DEFAULT '[]'::jsonb, - tags JSONB NOT NULL DEFAULT '[]'::jsonb, - layout JSONB NOT NULL, -- TemplateNode[] 트리 - semantic_description TEXT, - domain_keywords JSONB DEFAULT '[]'::jsonb, - embedding VECTOR(768), - visibility_scope VARCHAR(32) NOT NULL DEFAULT 'company', - allowed_role_ids JSONB, - created_by VARCHAR(64) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - version INT NOT NULL DEFAULT 1 -); -CREATE INDEX idx_templates_company ON invyone_builder.templates(company_code); -CREATE INDEX idx_templates_category ON invyone_builder.templates USING GIN(category); - --- ============================================================ --- 3. dashboards — 사용자 작업 공간 --- ============================================================ -CREATE TABLE invyone_builder.dashboards ( - dashboard_id VARCHAR(64) PRIMARY KEY, -- UUID - company_code VARCHAR(64) NOT NULL, - owner_id VARCHAR(64) NOT NULL, - name_ko VARCHAR(255) NOT NULL, - is_default BOOLEAN NOT NULL DEFAULT FALSE, - sort_order INT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -CREATE INDEX idx_dashboards_owner ON invyone_builder.dashboards(company_code, owner_id); - --- ============================================================ --- 4. dashboard_items — Dashboard 안의 Template 인스턴스 --- ============================================================ -CREATE TABLE invyone_builder.dashboard_items ( - item_id VARCHAR(64) PRIMARY KEY, - dashboard_id VARCHAR(64) NOT NULL REFERENCES invyone_builder.dashboards(dashboard_id) ON DELETE CASCADE, - template_id VARCHAR(64) NOT NULL, -- FK 안 검 (시스템/회사 양쪽 가능) - display_name VARCHAR(255), - -- 위치 (시안의 _xp/_wp/_y/_h) - x_ratio REAL NOT NULL DEFAULT 0, - width_ratio REAL NOT NULL DEFAULT 0.5, - y_px INT NOT NULL DEFAULT 20, - height_px INT NOT NULL DEFAULT 500, - -- 접힘 상태 - is_collapsed BOOLEAN NOT NULL DEFAULT FALSE, - collapsed_x INT, - collapsed_y INT, - collapsed_w INT, - collapsed_h INT, - card_style VARCHAR(32) DEFAULT 'flat', - card_bg_image TEXT, - z_index INT NOT NULL DEFAULT 100, - -- AI 흔적 - generated_by_ai BOOLEAN NOT NULL DEFAULT FALSE, - generated_from_prompt TEXT, - sort_order INT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -CREATE INDEX idx_items_dashboard ON invyone_builder.dashboard_items(dashboard_id); - --- ============================================================ --- 5. company_themes — 회사 화이트라벨링 --- ============================================================ -CREATE TABLE invyone_builder.company_themes ( - company_code VARCHAR(64) PRIMARY KEY, - nav_position VARCHAR(16) NOT NULL DEFAULT 'left', -- top|left|right|bottom - theme_color VARCHAR(16) NOT NULL DEFAULT '#4a6cf7', - nav_bg_color VARCHAR(16) NOT NULL DEFAULT '#ffffff', - nav_text_color VARCHAR(16) NOT NULL DEFAULT '#333333', - nav_icon_color VARCHAR(16) NOT NULL DEFAULT '#333333', - bg_color VARCHAR(16) NOT NULL DEFAULT '#f5f6f8', - font_family VARCHAR(255) NOT NULL DEFAULT 'Pretendard', - font_size_px INT NOT NULL DEFAULT 14, - logo_image_url TEXT, - seal_image_url TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -``` - -### 11.4 마이그레이션 정책 - -- 신규 schema 라 기존 데이터와 충돌 0 -- M1 시작 전 위 SQL 1개 스크립트로 일괄 적용 (`db/migrations/V100__invyone_builder_init.sql` 같은 신규 파일) -- 시스템 기본 Component 76개는 seed SQL 로 같이 INSERT -- 시스템 기본 Template 은 M2 에서 hr-management 등 1~2개 우선 seed - -### 11.5 기존 스키마와의 FK - -- `dashboards.owner_id` → `USER_INFO.user_id` (FK 안 거는 게 안전. 멀티테넌시 / cross-DB 가능성) -- `dashboards.company_code` → `COMPANY_INFO.company_code` (동일) -- → 무결성은 application layer 에서 보장 - ---- - -## 12. 마일스톤 - -| ID | 이름 | 정의 | 산출물 | +| 항목 | 기존 | 시안 | 결정 | |---|---|---|---| -| **M1** | **빌더 UX 골격** | HTML 시안 그대로의 사용자 모드 빌더 + 컴포넌트 라이브러리 모달 + placeholder 카드 + 드래그/리사이즈/접기/펴기 + localStorage | `(invyone)/dashboard/[id]` 페이지, ComponentLibrary TS 모듈, Dashboard / Item 메타 (메모리만, DB 미연결) | -| **M2** | **DB 연동** | 위 5개 테이블 마이그레이션 + Spring 컨트롤러/서비스/매퍼 + frontend API 클라이언트 + 진짜 저장/로드 | `db/migrations/V100*.sql`, backend-spring 5개 컨트롤러, frontend `lib/api/builder.ts` | -| **M3** | **개발자 모드** | `(invyone)/builder` 라우트 + 컴포넌트 조립 캔버스 + 속성 패널 + Template 저장. 권한 가드. | `(invyone)/builder/page.tsx`, builder state store, role 가드 | -| **M4** | **컴포넌트 실구현 1차** | placeholder → 실제 React 컴포넌트로 교체. 우선순위: data-table, kpi-card, bar-chart, line-chart, input-form, search-filter (이거 6개로 hr-management 재현) | `frontend/components/invyone/{kpi-card,data-table,...}` | -| **M5** | **회사 화이트라벨링 + 설정 페이지 3개** | settings/profile, settings/company, settings/theme. company_themes 테이블 사용. | `(invyone)/settings/*` | -| **M6** | **AI 어시스턴트 stub → 실연결** | ai-assistant 의 Gemini → 로컬 LLM 어댑터. semantic_description / embedding 채우기. /api/ai/v1/builder/intent 활성. "수주 만들어줘" 시나리오 PoC | ai-assistant 새 엔드포인트, embedding seed 작업 | -| **M7~** | **ERP 도메인 파이프라인 빌드** | BOM, 생산, 출하, 세금계산서, 물류 각각 별도 파이프라인으로 신규 구축. 각 도메인마다 별도 SPEC 작성. | 도메인별 SPEC + 생성 코드 | - -### 12.1 M1 의 정확한 정의 (Done 기준) - -- [ ] `(invyone)` 라우트 그룹 + layout 생성 -- [ ] `/invyone/dashboard/[id]` 페이지 생성 -- [ ] ComponentLibrary 데이터 (76개) 를 `frontend/lib/invyone/component-catalog.ts` 로 옮김 -- [ ] TemplateLibrary 데이터 (시안의 18개 시스템 템플릿) 를 `frontend/lib/invyone/template-catalog.ts` 로 옮김 (placeholder 메타만) -- [ ] 헤더 (햄버거 + 로고 + 탭nav + 모드토글 + 사용자드롭다운) -- [ ] 사이드바 (대시보드 목록 + 추가 폼) — 시안 그대로 -- [ ] 빈 상태 + "+ 템플릿 추가하기" 버튼 -- [ ] 템플릿 라이브러리 모달 (카테고리 탭 + 태그 칩 + 검색 + 그리드) -- [ ] Template 클릭 → 캔버스에 placeholder 카드 추가 -- [ ] 편집 모드 토글 -- [ ] 카드 드래그 이동 (xRatio/yPx) -- [ ] 카드 리사이즈 (4방향 핸들, widthRatio/heightPx) -- [ ] 카드 접기/펴기 -- [ ] 카드 전체화면 -- [ ] 카드 삭제 -- [ ] localStorage 에 dashboards 상태 저장/복원 -- [ ] 인증: 기존 invyone 의 로그인 거쳐서 진입 (별도 구현 X, JWT claims 만 읽음) -- [ ] AI 진입점 stub 버튼 (클릭 시 "M5 에서 활성화" 토스트) - -### 12.2 M1 에서 의도적으로 안 하는 것 -- 진짜 컴포넌트 렌더 (placeholder 만) -- DB 저장 (localStorage 만) -- 개발자 모드 (M3) -- 회사 화이트라벨링 (M5) -- AI 실연결 (M6) -- 다중 사용자 동시편집 +| Element 단위 | 차트 1개 (`type: 'chart', subtype: 'bar'`) | 큰 화면 1개 (`tpl-hr-mgmt`) | **DashboardElement 에 `type: 'template'` 추가**. 기존 `'chart'` 도 유지 | +| dataSource | 각 element 가 `dataSource: { query: "SELECT ..." }` 자체 보유 | 템플릿이 자체적으로 알아서 fetch | Template 컴포넌트 안에서 React Query/SWR 로 직접 호출. element 에는 templateId 만 | +| 편집 라우트 | `/admin/screenMng/dashboardList?load=...` (Screen Designer 계열) | 별도 개발자 모드 | 첫 마일스톤은 **편집 X, 미리 만든 dashboard 1개를 보여주기만**. 편집은 M2 | +| 사용자/개발자 모드 분리 | 없음 | 헤더의 mode-switch 토글 | M2 이후 | +| 화이트라벨링 옵션 패널 | 없음 | 네비위치/테마컬러/폰트 등 | M3 이후 | --- -## 13. 비범위 / 미결정 +## 3. 메타 모델 -### 13.1 비범위 (이번 SPEC 의 책임 X) -- ERP 도메인 비즈니스 로직 (각 도메인 별도 SPEC) -- 모바일 전용 UX (M7 이후) -- 다국어 (M7 이후) -- 기존 invyone 데이터 마이그레이션 (별개 프로젝트로 취급) +### 3.1 3계층 -### 13.2 미결정 (사용자 결정 필요) -1. **M1 의 위치 — `(invyone)` 새 라우트 그룹 vs 기존 `(main)/invyone-builder` 하위** — SPEC 은 새 라우트 그룹 추천. 사용자 확인 필요. -2. **로컬 LLM 모델 / 임베딩 모델 / 차원** — M5 까지 결정 필요. 일단 schema 의 `vector(768)` 은 잠정. -3. **벡터 DB 위치** — pgvector (같은 DB) 추천. 별도 Qdrant/Weaviate 가도 됨. -4. **`templates.layout` 트리의 자식 중첩 깊이** — SPEC 은 1단계만 (시안과 동일). 무한 중첩 허용 시 빌더 UX/LLM 추론 둘 다 폭발적으로 어려워짐. -5. **시스템 기본 Template seed 의 첫 묶음** — M2 시점에 어떤 템플릿부터 진짜로 만들지. 시안에는 18개 메타가 있는데 그 중 hr-management 만 templates/hr-management.html 로 실제 mini-app 존재. 우선순위 결정 필요. -6. **개발자 모드 권한 — `DEV` 라는 user_type 이 기존 invyone 에 있는가?** — 기존 USER_INFO.user_type 의 가능한 값 확인 필요. 없으면 추가 또는 기존 값에 매핑. -7. **AI 어시스턴트 입력 위치** — 헤더 우측 상시 입력창 vs 사이드 채팅 패널 vs 캔버스 위 플로팅 버튼. 시안에 명시 X. +``` +Component ─ React 컴포넌트. 코드. 메타 모델 아님. (= 그냥 우리가 짜는 코드) + │ + └─▶ Template ─ "큰 화면 한 덩이" 단위. 예: 인사정보, 매출 대시보드. + │ Registry 에 { id, name, icon, category, tags, render } 로 등록. + │ 각각 하나의 React 컴포넌트. + │ + └─▶ Dashboard ─ 사용자가 자기 작업 공간에 만든 캔버스. + DashboardElement[] 를 가짐. 각 element 는 + { id, type:'template', templateId, position, size, ... } +``` + +**Component 는 메타 모델 아님** — 시안의 80개 컴포넌트 카탈로그처럼 DB 에 박지 않는다. 그냥 코드. Template 을 만들 때 React 코드 안에서 자유롭게 import 해서 쓴다. + +### 3.2 Template Registry (TS 코드) + +```ts +// frontend/lib/templates/registry.ts +import { HrEmployeeList } from "@/components/templates/HrEmployeeList"; + +export type TemplateRenderer = React.ComponentType<{ + config?: Record; +}>; + +export interface TemplateMeta { + id: string; // 'hr-employee-list' + name: string; // '인사정보' + description: string; // '사원 목록, 부서/역할 조회' + icon: string; // '👥' + category: 'erp' | 'mes' | 'scm' | 'admin' | 'dashboard'; + tags: string[]; // ['ERP', '인사/급여'] + defaultSize: { w: number; h: number }; // 픽셀 + render: TemplateRenderer; +} + +export const TEMPLATE_REGISTRY: Record = { + 'hr-employee-list': { + id: 'hr-employee-list', + name: '인사정보', + description: '사원 목록, 부서/역할/상태 조회', + icon: '👥', + category: 'erp', + tags: ['ERP', '인사/급여'], + defaultSize: { w: 960, h: 640 }, + render: HrEmployeeList, + }, +}; +``` + +첫 마일스톤에서 Registry 의 엔트리는 **딱 1개**. 이후 마일스톤마다 1~2개씩 추가. + +### 3.3 DashboardElement 타입 변경 + +```ts +// frontend/components/admin/dashboard/types.ts (수정) +export type DashboardElement = + | DashboardChartElement // 기존 — 그대로 유지, 안 건드림 + | DashboardTemplateElement; // ★ NEW + +export interface DashboardTemplateElement { + id: string; + type: 'template'; + templateId: string; // TEMPLATE_REGISTRY 의 키 + position: { x: number; y: number }; + size: { width: number; height: number }; + title?: string; // 카드 헤더 표시용 (없으면 TEMPLATE_REGISTRY[].name) + collapsed?: boolean; + config?: Record; // 템플릿별 옵션 (예: { defaultDept: 'IT' }) +} +``` + +기존 chart element 는 그대로 둔다. DashboardViewer 는 `element.type` 으로 분기. --- -## 14. 오픈 이슈 +## 4. 첫 마일스톤 (M1) — 인사정보 카드 1개 -| # | 항목 | 상태 | +### 4.1 정의 +사용자가 invyone 에 로그인 → 사이드바의 "대시보드" 클릭 → "인사 대시보드" 진입 → 화면에 **인사정보 템플릿 카드 1개가 v5 Cosmic 디자인으로 그려져 있고, 그 안에 실제 USER_INFO/DEPARTMENT/ROLE_INFO 조인 데이터가 표 형태로 보임.** + +### 4.2 완료 조건 (Definition of Done) +1. `/dashboard` 진입 시 "인사 대시보드" 라는 dashboard 카드가 1개 보임 +2. 그걸 클릭하면 `/dashboard/{id}` 로 이동 +3. 그 페이지에 인사정보 template 카드가 그려짐 (절대 위치, defaultSize) +4. 카드 안에 통계카드 4개(전체/재직/휴직/퇴직) + 검색 필터 + 사원 테이블이 v5 Cosmic 톤으로 표시됨 +5. 테이블 데이터는 백엔드 API (`GET /api/hr/employees`) 에서 진짜 옴 +6. 검색/페이지네이션 동작 +7. 다크/라이트 모드 둘 다 깨지지 않음 +8. 빌드 통과 (`npm run build`, `./gradlew bootJar`) + +### 4.3 작업 리스트 (구체적) + +**Frontend** + +1. `frontend/lib/templates/registry.ts` 생성 — `TEMPLATE_REGISTRY` 정의 +2. `frontend/components/templates/HrEmployeeList.tsx` 생성 + - v5 토큰 사용 (`v5-card`, `var(--v5-glass)`, `var(--v5-primary)`) + - 통계카드 4개 (전체/재직/휴직/퇴직 — STATUS 컬럼 기준) + - 검색 필터바 (이름/사번/부서) + - 사원 테이블 (USER_NAME, USER_ID, 부서, 역할, STATUS, 입사일) + - 페이지네이션 + - React Query 또는 SWR 로 `/api/hr/employees` 호출 +3. `frontend/components/admin/dashboard/types.ts` 수정 — `DashboardTemplateElement` 추가, union 타입 갱신 +4. `frontend/components/dashboard/DashboardViewer.tsx` 수정 — `element.type === 'template'` 분기 추가, `TEMPLATE_REGISTRY[templateId].render` 호출 +5. `frontend/lib/api/dashboard.ts` — 기존 API 가 죽어있으면 임시로 `getDashboards()` 가 "인사 대시보드" 1개를 하드코딩 반환하게 (M1 한정. M2 에서 진짜 백엔드 연결) +6. `frontend/lib/api/hrEmployee.ts` 신규 — `getEmployees({ page, size, search })` + +**Backend** + +7. `backend-spring/src/main/java/com/erp/controller/HrEmployeeController.java` 신규 — `GET /api/hr/employees` +8. `backend-spring/src/main/java/com/erp/service/HrEmployeeService.java` 신규 — BaseService 패턴 +9. `backend-spring/src/main/resources/mapper/hrEmployee.xml` 신규 — USER_INFO + DEPARTMENT + ROLE_INFO 조인 SELECT, `common.companyCodeFilter` include +10. `SecurityConfig.java` — `/api/hr/**` 인증 경로 등록 (이미 패턴 있으면 추가만) + +**검증** + +11. `cd frontend && npm run build` 통과 +12. `cd backend-spring && ./gradlew bootJar` 통과 +13. 도커 컨테이너 (`docker-compose.invyone.yml`) 재기동 후 브라우저로 동작 확인 + +### 4.4 비범위 (M1 안 함) +- Dashboard 편집 UX (드래그/리사이즈/추가/삭제) — M2 +- Template 라이브러리 모달 — M2 +- 사용자/개발자 모드 분리 — M3 +- 화이트라벨링 옵션 패널 — M3 +- 인사정보 외 다른 Template — M2 부터 점진 추가 +- 사원 등록/수정/삭제 — M2 (M1 은 조회만) +- AI 어시스턴트 연결 — M5 + +--- + +## 5. 디자인 — v5 Cosmic (★ 변경 없음) + +### 5.1 절대 준수 +- **모든 색상 / 간격 / 글래스 / 그림자는 `frontend/styles/v5-layout.css` 의 토큰만 사용** +- 새 hex/rgb 즉흥 사용 금지 +- 시안의 `#4a6cf7`, `#27ae60`, `#e74c3c` 등 가져오지 말 것 — 같은 의미는 v5 토큰으로 매핑 + +### 5.2 시안 → v5 토큰 매핑 (인사정보 화면용) + +| 시안 (hr-management.html) | v5 토큰 | +|---|---| +| `#4a6cf7` (주 액센트) | `var(--v5-primary)` (#6c5ce7) | +| `#27ae60` (성공/재직) | `var(--v5-success)` 또는 v5-bdg-success 클래스 | +| `#f39c12` (경고/휴직) | `var(--v5-warn)` | +| `#e74c3c` (위험/퇴직) | `var(--v5-danger)` | +| `#fff` 카드 배경 | `var(--v5-glass)` + `backdrop-filter: blur(20px) saturate(1.4)` | +| `#e8e8e8` 보더 | `var(--v5-glass-border)` | +| `box-shadow: 0 2px 12px rgba(0,0,0,0.06)` | `var(--v5-glow-sm)` | +| 폰트 사이즈 13px | v5 컴팩트 스케일 (0.6~0.85rem) | + +### 5.3 새 클래스가 필요하면 +- 일회성 inline ` + + + +
+ 개별 + + + + + + + + 조합 + + +
+ +
+
+ +
홈 › 대시보드
+
+
관리자 모드
+ +
+
+ +
+ +
USER MODE
+
+
+ + + + +