This commit is contained in:
@@ -50,7 +50,8 @@ public class ProvisioningController {
|
|||||||
|
|
||||||
private static final Set<String> RESERVED_SUBDOMAINS = Set.of(
|
private static final Set<String> RESERVED_SUBDOMAINS = Set.of(
|
||||||
"www", "admin", "api", "app", "static", "assets",
|
"www", "admin", "api", "app", "static", "assets",
|
||||||
"main", "mail", "blog", "dev", "test", "staging", "prod", "console"
|
"main", "mail", "blog", "dev", "test", "staging", "prod", "console",
|
||||||
|
"solution"
|
||||||
);
|
);
|
||||||
|
|
||||||
@GetMapping("/table-groups")
|
@GetMapping("/table-groups")
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ RUN ./gradlew bootJar --no-daemon
|
|||||||
FROM eclipse-temurin:21-jre-alpine AS runner
|
FROM eclipse-temurin:21-jre-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache curl
|
# postgresql16-client: 회사 프로비저닝 시 pg_dump 로 스키마 복사.
|
||||||
|
# 서버 PG 16.x 와 버전 맞춤 (alpine 기본 postgresql-client 는 18 이라 불일치).
|
||||||
|
RUN apk add --no-cache curl postgresql16-client
|
||||||
|
|
||||||
COPY --from=build /app/build/libs/*.jar app.jar
|
COPY --from=build /app/build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
|||||||
@@ -81,17 +81,75 @@ export default function SubdomainListPage() {
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
background: "var(--v5-bg)",
|
background: "var(--v5-bg)",
|
||||||
color: "var(--v5-text)",
|
color: "var(--v5-text)",
|
||||||
fontFamily: "var(--v5-font-sans)",
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes pulsedot { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
@keyframes pulsedot { 0%,100% { opacity: 1; } 50% { opacity: 0.35; transform: scale(0.88); } }
|
||||||
|
@keyframes provPageFadeUp {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes provPageFade {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
[data-accrow]:last-child { border-bottom: 0 !important; }
|
[data-accrow]:last-child { border-bottom: 0 !important; }
|
||||||
|
[data-prov-enter] {
|
||||||
|
animation: provPageFadeUp 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
[data-prov-enter="1"] { animation-delay: 0ms; }
|
||||||
|
[data-prov-enter="2"] { animation-delay: 70ms; }
|
||||||
|
[data-prov-enter="3"] { animation-delay: 140ms; }
|
||||||
|
[data-prov-enter="4"] { animation-delay: 210ms; }
|
||||||
|
[data-prov-enter="5"] { animation-delay: 280ms; }
|
||||||
|
|
||||||
|
[data-prov-row] {
|
||||||
|
transition: background 0.18s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
animation: provPageFade 0.4s ease-out both;
|
||||||
|
}
|
||||||
|
[data-prov-row]:hover {
|
||||||
|
background: var(--v5-surface-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prov-hbtn {
|
||||||
|
transition: transform 0.18s cubic-bezier(0.4,0,0.2,1),
|
||||||
|
box-shadow 0.18s ease,
|
||||||
|
background 0.18s ease,
|
||||||
|
border-color 0.18s ease;
|
||||||
|
}
|
||||||
|
.prov-hbtn:not(:disabled):hover { transform: translateY(-1px); }
|
||||||
|
.prov-hbtn:not(:disabled):active { transform: translateY(0) scale(0.97); }
|
||||||
|
.prov-hbtn-primary:not(:disabled):hover {
|
||||||
|
box-shadow: 0 0 14px rgba(var(--v5-primary-rgb), 0.28) !important;
|
||||||
|
}
|
||||||
|
.prov-hbtn-secondary:not(:disabled):hover {
|
||||||
|
background: var(--v5-surface-hover) !important;
|
||||||
|
border-color: var(--v5-primary) !important;
|
||||||
|
color: var(--v5-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prov-pagebtn {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.prov-pagebtn:not(:disabled):hover {
|
||||||
|
background: var(--v5-surface-hover) !important;
|
||||||
|
border-color: var(--v5-primary) !important;
|
||||||
|
color: var(--v5-primary) !important;
|
||||||
|
}
|
||||||
|
.prov-pagebtn-active {
|
||||||
|
box-shadow: 0 0 5px rgba(var(--v5-primary-rgb), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prov-input:focus {
|
||||||
|
border-color: var(--v5-primary) !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--v5-primary-rgb), 0.15) !important;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
{/* 페이지 헤더 */}
|
{/* 페이지 헤더 */}
|
||||||
<div
|
<div
|
||||||
|
data-prov-enter="1"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-end",
|
alignItems: "flex-end",
|
||||||
@@ -112,7 +170,6 @@ export default function SubdomainListPage() {
|
|||||||
fontSize: "0.72rem",
|
fontSize: "0.72rem",
|
||||||
color: "var(--v5-text-sec)",
|
color: "var(--v5-text-sec)",
|
||||||
marginTop: "0.25rem",
|
marginTop: "0.25rem",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -123,8 +180,8 @@ export default function SubdomainListPage() {
|
|||||||
<HeaderBtn onClick={() => refetch()} icon={<RefreshCw size={11} strokeWidth={1.75} />}>
|
<HeaderBtn onClick={() => refetch()} icon={<RefreshCw size={11} strokeWidth={1.75} />}>
|
||||||
새로고침
|
새로고침
|
||||||
</HeaderBtn>
|
</HeaderBtn>
|
||||||
<HeaderBtn icon={<FileText size={11} strokeWidth={1.75} />}>감사 로그</HeaderBtn>
|
<HeaderBtn icon={<FileText size={11} strokeWidth={1.75} />} soon>감사 로그</HeaderBtn>
|
||||||
<HeaderBtn icon={<Download size={11} strokeWidth={1.75} />}>CSV 내보내기</HeaderBtn>
|
<HeaderBtn icon={<Download size={11} strokeWidth={1.75} />} soon>CSV 내보내기</HeaderBtn>
|
||||||
<HeaderBtn
|
<HeaderBtn
|
||||||
variant="primary"
|
variant="primary"
|
||||||
icon={<Plus size={11} strokeWidth={1.75} />}
|
icon={<Plus size={11} strokeWidth={1.75} />}
|
||||||
@@ -136,10 +193,13 @@ export default function SubdomainListPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* stats strip */}
|
{/* stats strip */}
|
||||||
<CompanyStatsStrip rows={rows} />
|
<div data-prov-enter="2">
|
||||||
|
<CompanyStatsStrip rows={rows} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* toolbar */}
|
{/* toolbar */}
|
||||||
<div
|
<div
|
||||||
|
data-prov-enter="3"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "-1.1rem",
|
marginLeft: "-1.1rem",
|
||||||
marginRight: "-1.1rem",
|
marginRight: "-1.1rem",
|
||||||
@@ -157,6 +217,7 @@ export default function SubdomainListPage() {
|
|||||||
value={q}
|
value={q}
|
||||||
onChange={(e) => setQ(e.target.value)}
|
onChange={(e) => setQ(e.target.value)}
|
||||||
placeholder="회사명 · subdomain · company_code 검색"
|
placeholder="회사명 · subdomain · company_code 검색"
|
||||||
|
className="prov-input"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: "0.4rem 0.45rem 0.4rem 1.7rem",
|
padding: "0.4rem 0.45rem 0.4rem 1.7rem",
|
||||||
@@ -167,6 +228,7 @@ export default function SubdomainListPage() {
|
|||||||
fontSize: "0.72rem",
|
fontSize: "0.72rem",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
|
transition: "all 0.18s ease",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -214,11 +276,12 @@ export default function SubdomainListPage() {
|
|||||||
|
|
||||||
{/* list header */}
|
{/* list header */}
|
||||||
<div
|
<div
|
||||||
|
data-prov-enter="4"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "-1.1rem",
|
marginLeft: "-1.1rem",
|
||||||
marginRight: "-1.1rem",
|
marginRight: "-1.1rem",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "14px 1fr 110px 100px 80px 18px",
|
gridTemplateColumns: "14px minmax(280px, 420px) minmax(160px, 1fr) 90px 110px 110px 100px 80px 18px",
|
||||||
gap: "0.85rem",
|
gap: "0.85rem",
|
||||||
padding: "0.65rem 2rem",
|
padding: "0.65rem 2rem",
|
||||||
fontSize: "0.7rem",
|
fontSize: "0.7rem",
|
||||||
@@ -231,6 +294,9 @@ export default function SubdomainListPage() {
|
|||||||
>
|
>
|
||||||
<span />
|
<span />
|
||||||
<span>회사 / 서브도메인</span>
|
<span>회사 / 서브도메인</span>
|
||||||
|
<span>담당자</span>
|
||||||
|
<span>플랜</span>
|
||||||
|
<span>생성일</span>
|
||||||
<span>사용자</span>
|
<span>사용자</span>
|
||||||
<span>DB 용량</span>
|
<span>DB 용량</span>
|
||||||
<span>상태</span>
|
<span>상태</span>
|
||||||
@@ -239,6 +305,7 @@ export default function SubdomainListPage() {
|
|||||||
|
|
||||||
{/* rows (scrollable when long) */}
|
{/* rows (scrollable when long) */}
|
||||||
<div
|
<div
|
||||||
|
data-prov-enter="5"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "-1.1rem",
|
marginLeft: "-1.1rem",
|
||||||
marginRight: "-1.1rem",
|
marginRight: "-1.1rem",
|
||||||
@@ -331,16 +398,21 @@ function HeaderBtn({
|
|||||||
icon,
|
icon,
|
||||||
variant = "secondary",
|
variant = "secondary",
|
||||||
onClick,
|
onClick,
|
||||||
|
soon,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
variant?: "primary" | "secondary";
|
variant?: "primary" | "secondary";
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
soon?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const isPrimary = variant === "primary";
|
const isPrimary = variant === "primary";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={soon ? undefined : onClick}
|
||||||
|
disabled={soon}
|
||||||
|
title={soon ? "준비 중인 기능입니다" : undefined}
|
||||||
|
className={soon ? undefined : `prov-hbtn prov-hbtn-${isPrimary ? "primary" : "secondary"}`}
|
||||||
style={{
|
style={{
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -353,15 +425,31 @@ function HeaderBtn({
|
|||||||
border: `1px solid ${isPrimary ? "var(--v5-primary)" : "var(--v5-border)"}`,
|
border: `1px solid ${isPrimary ? "var(--v5-primary)" : "var(--v5-border)"}`,
|
||||||
background: isPrimary ? "var(--v5-primary)" : "var(--v5-surface-solid)",
|
background: isPrimary ? "var(--v5-primary)" : "var(--v5-surface-solid)",
|
||||||
color: isPrimary ? "#fff" : "var(--v5-text)",
|
color: isPrimary ? "#fff" : "var(--v5-text)",
|
||||||
boxShadow: isPrimary ? "0 0 18px rgba(var(--v5-primary-rgb), 0.22)" : "none",
|
boxShadow: isPrimary && !soon ? "0 0 10px rgba(var(--v5-primary-rgb), 0.14)" : "none",
|
||||||
cursor: "pointer",
|
cursor: soon ? "not-allowed" : "pointer",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
transition: "all 0.15s",
|
opacity: soon ? 0.55 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{children}
|
{children}
|
||||||
|
{soon && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.58rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
padding: "1px 5px",
|
||||||
|
borderRadius: 3,
|
||||||
|
background: "rgba(var(--v5-amber-rgb), 0.18)",
|
||||||
|
color: "var(--v5-amber)",
|
||||||
|
letterSpacing: "0.03em",
|
||||||
|
marginLeft: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
준비 중
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -389,6 +477,7 @@ function Pagination({
|
|||||||
key={key}
|
key={key}
|
||||||
onClick={() => !disabled && onChange(p)}
|
onClick={() => !disabled && onChange(p)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
className={`prov-pagebtn ${active ? "prov-pagebtn-active" : ""}`}
|
||||||
style={{
|
style={{
|
||||||
minWidth: 24,
|
minWidth: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -19,6 +19,13 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import StatusDot from "./StatusDot";
|
import StatusDot from "./StatusDot";
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ key: "overview", label: "개요", Icon: Info, soon: false },
|
||||||
|
{ key: "members", label: "구성원", Icon: Users, soon: true },
|
||||||
|
{ key: "templates", label: "템플릿", Icon: Layers, soon: true },
|
||||||
|
{ key: "danger", label: "위험 영역", Icon: AlertTriangle, soon: false },
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 단일 회사 row. 클릭 시 accordion 펼쳐지며 탭 4개 (개요/구성원/템플릿/위험영역) 표시.
|
* 단일 회사 row. 클릭 시 accordion 펼쳐지며 탭 4개 (개요/구성원/템플릿/위험영역) 표시.
|
||||||
* 목업 v9 AccRow 포팅. API 데이터 스키마는 /companies-stats 응답.
|
* 목업 v9 AccRow 포팅. API 데이터 스키마는 /companies-stats 응답.
|
||||||
@@ -33,6 +40,20 @@ export default function CompanyAccordionRow({
|
|||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<"overview" | "members" | "templates" | "danger">("overview");
|
const [tab, setTab] = useState<"overview" | "members" | "templates" | "danger">("overview");
|
||||||
|
const tabsWrapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tabBtnRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
||||||
|
const [indicator, setIndicator] = useState<{ left: number; width: number }>({ left: 0, width: 0 });
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const el = tabBtnRefs.current[tab];
|
||||||
|
const wrap = tabsWrapRef.current;
|
||||||
|
if (el && wrap) {
|
||||||
|
const r1 = el.getBoundingClientRect();
|
||||||
|
const r2 = wrap.getBoundingClientRect();
|
||||||
|
setIndicator({ left: r1.left - r2.left, width: r1.width });
|
||||||
|
}
|
||||||
|
}, [tab, open]);
|
||||||
|
|
||||||
const sub = r.subdomain || "";
|
const sub = r.subdomain || "";
|
||||||
const name = r.company_name || r.name || r.company_code;
|
const name = r.company_name || r.name || r.company_code;
|
||||||
@@ -45,25 +66,26 @@ export default function CompanyAccordionRow({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-accrow
|
data-accrow
|
||||||
|
data-prov-row
|
||||||
style={{
|
style={{
|
||||||
background: open ? "var(--v5-surface-hover)" : "var(--v5-surface-solid)",
|
background: open ? "var(--v5-surface-hover)" : "var(--v5-surface-solid)",
|
||||||
borderBottom: "1px solid var(--v5-border)",
|
borderBottom: "1px solid var(--v5-border)",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
transition: "background 0.12s",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{open && (
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
position: "absolute",
|
||||||
position: "absolute",
|
left: 0,
|
||||||
left: 0,
|
top: 0,
|
||||||
top: 0,
|
bottom: 0,
|
||||||
bottom: 0,
|
width: 2,
|
||||||
width: 2,
|
background: "var(--v5-primary)",
|
||||||
background: "var(--v5-primary)",
|
transform: open ? "scaleY(1)" : "scaleY(0)",
|
||||||
}}
|
transformOrigin: "center",
|
||||||
/>
|
transition: "transform 0.25s cubic-bezier(0.4,0,0.2,1)",
|
||||||
)}
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
@@ -75,35 +97,26 @@ export default function CompanyAccordionRow({
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
padding: "0.6rem 2rem 0.6rem 2rem",
|
padding: "0.6rem 2rem 0.6rem 2rem",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "14px 1fr 110px 100px 80px 18px",
|
gridTemplateColumns: "14px minmax(280px, 420px) minmax(160px, 1fr) 90px 110px 110px 100px 80px 18px",
|
||||||
gap: "0.85rem",
|
gap: "0.85rem",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{open ? (
|
<ChevronRight
|
||||||
<ChevronDown size={12} color="var(--v5-text-muted)" strokeWidth={1.75} />
|
size={12}
|
||||||
) : (
|
color={open ? "var(--v5-primary)" : "var(--v5-text-muted)"}
|
||||||
<ChevronRight size={12} color="var(--v5-text-muted)" strokeWidth={1.75} />
|
strokeWidth={1.75}
|
||||||
)}
|
style={{
|
||||||
|
transform: open ? "rotate(90deg)" : "rotate(0deg)",
|
||||||
|
transition: "transform 0.25s cubic-bezier(0.4,0,0.2,1), color 0.2s ease",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div style={{ minWidth: 0 }}>
|
<div style={{ minWidth: 0 }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 7 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 7 }}>
|
||||||
<span style={{ fontSize: "0.82rem", fontWeight: 700, letterSpacing: "-0.01em", color: "var(--v5-text)" }}>
|
<span style={{ fontSize: "0.82rem", fontWeight: 700, letterSpacing: "-0.01em", color: "var(--v5-text)" }}>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: "0.58rem",
|
|
||||||
padding: "1px 6px",
|
|
||||||
borderRadius: 3,
|
|
||||||
background: "var(--v5-border)",
|
|
||||||
color: "var(--v5-text)",
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: "0.04em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{plan.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -128,6 +141,87 @@ export default function CompanyAccordionRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 담당자 */}
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
{r.owner || r.representative_name ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.76rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--v5-text)",
|
||||||
|
lineHeight: 1.15,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.owner || r.representative_name}
|
||||||
|
</div>
|
||||||
|
{r.email && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
color: "var(--v5-text-muted)",
|
||||||
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
marginTop: 2,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.email}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!r.email && r.representative_phone && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
color: "var(--v5-text-muted)",
|
||||||
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.representative_phone}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: "0.72rem", color: "var(--v5-text-muted)" }}>—</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 플랜 */}
|
||||||
|
<div>
|
||||||
|
<PlanBadge plan={plan} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 생성일 */}
|
||||||
|
<div>
|
||||||
|
<div style={labelSm}>생성</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.72rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
color: "var(--v5-text)",
|
||||||
|
lineHeight: 1.1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatRelative(r.created) || "—"}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.58rem",
|
||||||
|
color: "var(--v5-text-muted)",
|
||||||
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatDate(r.created)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style={labelSm}>사용자</div>
|
<div style={labelSm}>사용자</div>
|
||||||
<div
|
<div
|
||||||
@@ -169,18 +263,37 @@ export default function CompanyAccordionRow({
|
|||||||
<MoreHorizontal size={12} color="var(--v5-text-muted)" strokeWidth={1.75} />
|
<MoreHorizontal size={12} color="var(--v5-text-muted)" strokeWidth={1.75} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
<div
|
||||||
<div style={{ padding: "0 2rem 0.9rem 3rem" }}>
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateRows: open ? "1fr" : "0fr",
|
||||||
|
transition: "grid-template-rows 0.3s cubic-bezier(0.4,0,0.2,1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ overflow: "hidden" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "0 2rem 0.9rem 3rem",
|
||||||
|
opacity: open ? 1 : 0,
|
||||||
|
transform: open ? "translateY(0)" : "translateY(-4px)",
|
||||||
|
transition: "opacity 0.25s ease 0.05s, transform 0.25s ease 0.05s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* tabs */}
|
{/* tabs */}
|
||||||
<div style={{ display: "flex", gap: 0, borderBottom: "1px solid var(--v5-border)", marginBottom: "0.7rem" }}>
|
<div
|
||||||
{([
|
ref={tabsWrapRef}
|
||||||
["overview", "개요", Info],
|
style={{
|
||||||
["members", "구성원", Users],
|
display: "flex",
|
||||||
["templates", "템플릿", Layers],
|
gap: 0,
|
||||||
["danger", "위험 영역", AlertTriangle],
|
borderBottom: "1px solid var(--v5-border)",
|
||||||
] as const).map(([k, l, IconC]) => (
|
marginBottom: "0.7rem",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TABS.map(({ key: k, label: l, Icon: IconC, soon }) => (
|
||||||
<button
|
<button
|
||||||
key={k}
|
key={k}
|
||||||
|
ref={(el) => { tabBtnRefs.current[k] = el; }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTab(k);
|
setTab(k);
|
||||||
@@ -193,15 +306,30 @@ export default function CompanyAccordionRow({
|
|||||||
fontSize: "0.72rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: tab === k ? "var(--v5-primary)" : "var(--v5-text-sec)",
|
color: tab === k ? "var(--v5-primary)" : "var(--v5-text-sec)",
|
||||||
borderBottom: `2px solid ${tab === k ? "var(--v5-primary)" : "transparent"}`,
|
|
||||||
marginBottom: -1,
|
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 5,
|
gap: 5,
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
|
transition: "color 0.2s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconC size={11} strokeWidth={1.75} /> {l}
|
<IconC size={11} strokeWidth={1.75} /> {l}
|
||||||
|
{soon && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.52rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
padding: "1px 5px",
|
||||||
|
borderRadius: 3,
|
||||||
|
background: "rgba(var(--v5-amber-rgb), 0.18)",
|
||||||
|
color: "var(--v5-amber)",
|
||||||
|
letterSpacing: "0.03em",
|
||||||
|
marginLeft: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
준비 중
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
@@ -219,6 +347,21 @@ export default function CompanyAccordionRow({
|
|||||||
{r.created && <>생성 {formatDate(r.created)}</>}
|
{r.created && <>생성 {formatDate(r.created)}</>}
|
||||||
{r.writer && <> · writer {r.writer}</>}
|
{r.writer && <> · writer {r.writer}</>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* sliding indicator */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -1,
|
||||||
|
left: indicator.left,
|
||||||
|
width: indicator.width,
|
||||||
|
height: 2,
|
||||||
|
background: "var(--v5-primary)",
|
||||||
|
boxShadow: "0 0 5px rgba(var(--v5-primary-rgb), 0.25)",
|
||||||
|
transition: "left 0.28s cubic-bezier(0.4,0,0.2,1), width 0.28s cubic-bezier(0.4,0,0.2,1)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === "overview" && (
|
{tab === "overview" && (
|
||||||
@@ -313,10 +456,10 @@ export default function CompanyAccordionRow({
|
|||||||
if (sub) window.open(`http://${sub}.invyone.com`, "_blank");
|
if (sub) window.open(`http://${sub}.invyone.com`, "_blank");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
테넌트 접속
|
회사 사이트 열기
|
||||||
</ABtn>
|
</ABtn>
|
||||||
<ABtn icon={<KeyRound size={11} strokeWidth={1.75} />}>관리자 계정</ABtn>
|
<ABtn icon={<KeyRound size={11} strokeWidth={1.75} />} soon>관리자 계정</ABtn>
|
||||||
<ABtn icon={<Copy size={11} strokeWidth={1.75} />}>템플릿 재복제</ABtn>
|
<ABtn icon={<Copy size={11} strokeWidth={1.75} />} soon>템플릿 재복제</ABtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -407,7 +550,8 @@ export default function CompanyAccordionRow({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -445,17 +589,22 @@ function ABtn({
|
|||||||
children,
|
children,
|
||||||
icon,
|
icon,
|
||||||
onClick,
|
onClick,
|
||||||
|
soon,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
|
soon?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={soon ? (e) => e.stopPropagation() : (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
}}
|
}}
|
||||||
|
disabled={soon}
|
||||||
|
title={soon ? "준비 중인 기능입니다" : undefined}
|
||||||
|
className={soon ? undefined : "prov-hbtn prov-hbtn-secondary"}
|
||||||
style={{
|
style={{
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -468,14 +617,30 @@ function ABtn({
|
|||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
color: "var(--v5-text)",
|
color: "var(--v5-text)",
|
||||||
cursor: "pointer",
|
cursor: soon ? "not-allowed" : "pointer",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
transition: "all 0.15s",
|
opacity: soon ? 0.55 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{children}
|
{children}
|
||||||
|
{soon && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.54rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
padding: "1px 5px",
|
||||||
|
borderRadius: 3,
|
||||||
|
background: "rgba(var(--v5-amber-rgb), 0.18)",
|
||||||
|
color: "var(--v5-amber)",
|
||||||
|
letterSpacing: "0.03em",
|
||||||
|
marginLeft: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
준비 중
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -512,3 +677,71 @@ function formatDate(v: any): string {
|
|||||||
return String(v);
|
return String(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRelative(v: any): string {
|
||||||
|
if (!v) return "";
|
||||||
|
try {
|
||||||
|
const d = typeof v === "number" ? new Date(v) : new Date(String(v));
|
||||||
|
if (isNaN(d.getTime())) return "";
|
||||||
|
const diffMs = Date.now() - d.getTime();
|
||||||
|
const sec = Math.round(diffMs / 1000);
|
||||||
|
if (sec < 60) return "방금";
|
||||||
|
const min = Math.round(sec / 60);
|
||||||
|
if (min < 60) return `${min}분 전`;
|
||||||
|
const hr = Math.round(min / 60);
|
||||||
|
if (hr < 24) return `${hr}시간 전`;
|
||||||
|
const day = Math.round(hr / 24);
|
||||||
|
if (day < 7) return `${day}일 전`;
|
||||||
|
const wk = Math.round(day / 7);
|
||||||
|
if (wk < 5) return `${wk}주 전`;
|
||||||
|
const mo = Math.round(day / 30);
|
||||||
|
if (mo < 12) return `${mo}개월 전`;
|
||||||
|
const yr = Math.round(day / 365);
|
||||||
|
return `${yr}년 전`;
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLAN_STYLE: Record<string, { bg: string; color: string; border: string }> = {
|
||||||
|
ENTERPRISE: {
|
||||||
|
bg: "rgba(var(--v5-primary-rgb), 0.12)",
|
||||||
|
color: "var(--v5-primary)",
|
||||||
|
border: "rgba(var(--v5-primary-rgb), 0.3)",
|
||||||
|
},
|
||||||
|
STANDARD: {
|
||||||
|
bg: "rgba(var(--v5-cyan-rgb), 0.1)",
|
||||||
|
color: "var(--v5-cyan)",
|
||||||
|
border: "rgba(var(--v5-cyan-rgb), 0.3)",
|
||||||
|
},
|
||||||
|
STARTER: {
|
||||||
|
bg: "var(--v5-bg-subtle)",
|
||||||
|
color: "var(--v5-text-sec)",
|
||||||
|
border: "var(--v5-border)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function PlanBadge({ plan }: { plan: string }) {
|
||||||
|
const key = plan.toUpperCase();
|
||||||
|
const s = PLAN_STYLE[key] || PLAN_STYLE.STARTER;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
padding: "3px 8px",
|
||||||
|
borderRadius: 4,
|
||||||
|
background: s.bg,
|
||||||
|
color: s.color,
|
||||||
|
border: `1px solid ${s.border}`,
|
||||||
|
letterSpacing: "0.06em",
|
||||||
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function StatusDot({ status }: { status?: string }) {
|
|||||||
height: 6,
|
height: 6,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
background: m.color,
|
background: m.color,
|
||||||
boxShadow: `0 0 6px ${m.color}`,
|
boxShadow: `0 0 3px ${m.color}`,
|
||||||
animation: isProvisioning ? "pulsedot 1.4s ease-in-out infinite" : "none",
|
animation: isProvisioning ? "pulsedot 1.4s ease-in-out infinite" : "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -105,43 +105,45 @@ export default function Step1Basic({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="wiz-step"
|
||||||
style={{
|
style={{
|
||||||
padding: "1.4rem 1.6rem",
|
padding: "1.6rem 1.8rem",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 320px",
|
gridTemplateColumns: "1fr 360px",
|
||||||
gap: "1.6rem",
|
gap: "1.8rem",
|
||||||
|
animation: "wizSlideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 왼쪽: 폼 */}
|
{/* 왼쪽: 폼 */}
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.55rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.15em",
|
letterSpacing: "0.18em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-cyan)",
|
color: "var(--v5-cyan)",
|
||||||
marginBottom: 6,
|
marginBottom: 8,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
01 · BASIC
|
01 · 기본 정보
|
||||||
</div>
|
</div>
|
||||||
<h2 style={{ fontSize: "1.15rem", fontWeight: 800, letterSpacing: "-0.02em", margin: 0, color: "var(--v5-text)" }}>
|
<h2 style={{ fontSize: "1.4rem", fontWeight: 800, letterSpacing: "-0.02em", margin: 0, color: "var(--v5-text)" }}>
|
||||||
회사 기본 정보
|
회사 기본 정보
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ fontSize: "0.7rem", color: "var(--v5-text-sec)", marginTop: 4 }}>
|
<div style={{ fontSize: "0.85rem", color: "var(--v5-text-sec)", marginTop: 6, fontWeight: 500 }}>
|
||||||
subdomain 을 입력하면 접속 URL 과 DB 이름이 자동 결정됩니다. 생성 후에는 변경 불가합니다.
|
subdomain 을 입력하면 접속 URL 과 DB 이름이 자동 결정됩니다. 생성 후에는 변경 불가합니다.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 식별자 ── */}
|
{/* ── 식별자 ── */}
|
||||||
<div style={{ marginTop: "1.2rem" }}>
|
<div style={{ marginTop: "1.4rem" }}>
|
||||||
<SectionHead
|
<SectionHead
|
||||||
icon={<LinkIcon size={11} strokeWidth={1.75} />}
|
icon={<LinkIcon size={13} strokeWidth={1.75} />}
|
||||||
label="식별자"
|
label="식별자"
|
||||||
hint="생성 후 변경 불가"
|
hint="생성 후 변경 불가"
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.7rem 0.85rem" }}>
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.9rem 1rem" }}>
|
||||||
<Field
|
<Field
|
||||||
label="SUBDOMAIN"
|
label="SUBDOMAIN"
|
||||||
required
|
required
|
||||||
@@ -150,7 +152,7 @@ export default function Step1Basic({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={state.subdomain || ""}
|
value={state.subdomain || ""}
|
||||||
onChange={onSubChange}
|
onChange={onSubChange}
|
||||||
placeholder="qnc"
|
placeholder="영문 소문자 · 숫자 · 하이픈"
|
||||||
suffix=".invyone.com"
|
suffix=".invyone.com"
|
||||||
mono
|
mono
|
||||||
status={availToInputStatus(subStatus)}
|
status={availToInputStatus(subStatus)}
|
||||||
@@ -164,7 +166,7 @@ export default function Step1Basic({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={state.db_prefix || ""}
|
value={state.db_prefix || ""}
|
||||||
onChange={onDbPrefixChange}
|
onChange={onDbPrefixChange}
|
||||||
placeholder="qnc"
|
placeholder="영문 소문자 · 숫자 · 밑줄"
|
||||||
suffix="_vexplor"
|
suffix="_vexplor"
|
||||||
mono
|
mono
|
||||||
status={availToInputStatus(prefStatus)}
|
status={availToInputStatus(prefStatus)}
|
||||||
@@ -178,7 +180,7 @@ export default function Step1Basic({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={state.company_code || ""}
|
value={state.company_code || ""}
|
||||||
onChange={onCompanyCodeChange}
|
onChange={onCompanyCodeChange}
|
||||||
placeholder="QNC"
|
placeholder="영문 대문자 · 숫자 · 밑줄"
|
||||||
mono
|
mono
|
||||||
status={availToInputStatus(codeStatus)}
|
status={availToInputStatus(codeStatus)}
|
||||||
/>
|
/>
|
||||||
@@ -187,21 +189,21 @@ export default function Step1Basic({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={state.company_name || ""}
|
value={state.company_name || ""}
|
||||||
onChange={(v) => setState({ company_name: v })}
|
onChange={(v) => setState({ company_name: v })}
|
||||||
placeholder="큐엔씨 주식회사"
|
placeholder="화면·문서에 표시될 회사 이름"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 법인 정보 ── */}
|
{/* ── 법인 정보 ── */}
|
||||||
<div style={{ marginTop: "1.2rem" }}>
|
<div style={{ marginTop: "1.4rem" }}>
|
||||||
<SectionHead icon={<Briefcase size={11} strokeWidth={1.75} />} label="법인 정보" />
|
<SectionHead icon={<Briefcase size={13} strokeWidth={1.75} />} label="법인 정보" />
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.7rem 0.85rem" }}>
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.9rem 1rem" }}>
|
||||||
<Field label="사업자번호">
|
<Field label="사업자번호">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={state.business_registration_number || ""}
|
value={state.business_registration_number || ""}
|
||||||
onChange={(v) => setState({ business_registration_number: v })}
|
onChange={(v) => setState({ business_registration_number: v })}
|
||||||
placeholder="123-45-67890"
|
placeholder="xxx-xx-xxxxx"
|
||||||
mono
|
mono
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -209,14 +211,14 @@ export default function Step1Basic({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={state.representative_name || ""}
|
value={state.representative_name || ""}
|
||||||
onChange={(v) => setState({ representative_name: v })}
|
onChange={(v) => setState({ representative_name: v })}
|
||||||
placeholder="홍길동"
|
placeholder="대표자 성함"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="대표 연락처">
|
<Field label="대표 연락처">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={state.representative_phone || ""}
|
value={state.representative_phone || ""}
|
||||||
onChange={(v) => setState({ representative_phone: v })}
|
onChange={(v) => setState({ representative_phone: v })}
|
||||||
placeholder="02-1234-5678"
|
placeholder="02-0000-0000"
|
||||||
mono
|
mono
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -224,7 +226,7 @@ export default function Step1Basic({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={state.email || ""}
|
value={state.email || ""}
|
||||||
onChange={(v) => setState({ email: v })}
|
onChange={(v) => setState({ email: v })}
|
||||||
placeholder="admin@company.kr"
|
placeholder="admin@example.com"
|
||||||
mono
|
mono
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -232,14 +234,14 @@ export default function Step1Basic({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={state.address || ""}
|
value={state.address || ""}
|
||||||
onChange={(v) => setState({ address: v })}
|
onChange={(v) => setState({ address: v })}
|
||||||
placeholder="서울특별시 강남구 테헤란로 123"
|
placeholder="도로명 주소"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="웹사이트" full>
|
<Field label="웹사이트" full>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={state.website || ""}
|
value={state.website || ""}
|
||||||
onChange={(v) => setState({ website: v })}
|
onChange={(v) => setState({ website: v })}
|
||||||
placeholder="https://company.kr"
|
placeholder="https://"
|
||||||
mono
|
mono
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -251,25 +253,25 @@ export default function Step1Basic({
|
|||||||
<div style={{ position: "sticky", top: 0, alignSelf: "start" }}>
|
<div style={{ position: "sticky", top: 0, alignSelf: "start" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.95rem 1rem",
|
padding: "1.1rem 1.2rem",
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
boxShadow: "0 0 24px rgba(var(--v5-cyan-rgb), 0.08)",
|
boxShadow: "0 0 14px rgba(var(--v5-cyan-rgb), 0.05)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.52rem",
|
fontSize: "0.68rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.18em",
|
letterSpacing: "0.2em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-cyan)",
|
color: "var(--v5-cyan)",
|
||||||
marginBottom: "0.65rem",
|
marginBottom: "0.85rem",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
LIVE PREVIEW
|
실시간 미리보기
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PreviewField label="접속 URL">
|
<PreviewField label="접속 URL">
|
||||||
@@ -283,7 +285,7 @@ export default function Step1Basic({
|
|||||||
</PreviewField>
|
</PreviewField>
|
||||||
|
|
||||||
<PreviewField label="DB 이름">
|
<PreviewField label="DB 이름">
|
||||||
<Database size={11} color="var(--v5-text-muted)" />
|
<Database size={13} color="var(--v5-text-muted)" />
|
||||||
<span style={{ color: "var(--v5-primary)", fontWeight: 700 }}>
|
<span style={{ color: "var(--v5-primary)", fontWeight: 700 }}>
|
||||||
{state.db_prefix || "___"}
|
{state.db_prefix || "___"}
|
||||||
</span>
|
</span>
|
||||||
@@ -300,20 +302,21 @@ export default function Step1Basic({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.6rem 0.7rem",
|
padding: "0.75rem 0.85rem",
|
||||||
background: "rgba(var(--v5-amber-rgb), 0.08)",
|
background: "rgba(var(--v5-amber-rgb), 0.08)",
|
||||||
border: "1px solid rgba(var(--v5-amber-rgb), 0.3)",
|
border: "1px solid rgba(var(--v5-amber-rgb), 0.3)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.78rem",
|
||||||
color: "var(--v5-amber)",
|
color: "var(--v5-amber)",
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
gap: 6,
|
gap: 7,
|
||||||
marginTop: "0.4rem",
|
marginTop: "0.55rem",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AlertTriangle size={11} strokeWidth={1.75} style={{ marginTop: 2, flexShrink: 0 }} />
|
<AlertTriangle size={14} strokeWidth={1.75} style={{ marginTop: 2, flexShrink: 0 }} />
|
||||||
<div>
|
<div>
|
||||||
<b>주의</b> — subdomain 과 db_prefix 는 생성 후 변경할 수 없습니다.
|
<b>주의</b> — subdomain 과 db_prefix 는 생성 후 변경할 수 없습니다.
|
||||||
</div>
|
</div>
|
||||||
@@ -336,15 +339,15 @@ function SectionHead({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.75rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "var(--v5-text-sec)",
|
color: "var(--v5-text-sec)",
|
||||||
letterSpacing: "0.1em",
|
letterSpacing: "0.1em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
marginBottom: "0.55rem",
|
marginBottom: "0.75rem",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 6,
|
gap: 7,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -357,6 +360,7 @@ function SectionHead({
|
|||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
|
fontSize: "0.72rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
— {hint}
|
— {hint}
|
||||||
@@ -368,30 +372,32 @@ function SectionHead({
|
|||||||
|
|
||||||
function PreviewField({ label, children }: { label: string; children: React.ReactNode }) {
|
function PreviewField({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: "0.85rem" }}>
|
<div style={{ marginBottom: "1rem" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.52rem",
|
fontSize: "0.68rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
letterSpacing: "0.08em",
|
letterSpacing: "0.08em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
marginBottom: 3,
|
marginBottom: 5,
|
||||||
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.5rem 0.6rem",
|
padding: "0.6rem 0.75rem",
|
||||||
background: "var(--v5-bg)",
|
background: "var(--v5-bg)",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.72rem",
|
fontSize: "0.85rem",
|
||||||
|
fontWeight: 500,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 5,
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -70,40 +70,42 @@ export default function Step2Template({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="wiz-step"
|
||||||
style={{
|
style={{
|
||||||
padding: "1.4rem 1.6rem",
|
padding: "1.6rem 1.8rem",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 280px",
|
gridTemplateColumns: "1fr 320px",
|
||||||
gap: "1.6rem",
|
gap: "1.8rem",
|
||||||
|
animation: "wizSlideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.55rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.15em",
|
letterSpacing: "0.18em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-cyan)",
|
color: "var(--v5-cyan)",
|
||||||
marginBottom: 6,
|
marginBottom: 8,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
02 · TEMPLATE
|
02 · 템플릿 선택
|
||||||
</div>
|
</div>
|
||||||
<h2 style={{ fontSize: "1.15rem", fontWeight: 800, letterSpacing: "-0.02em", margin: 0, color: "var(--v5-text)" }}>
|
<h2 style={{ fontSize: "1.4rem", fontWeight: 800, letterSpacing: "-0.02em", margin: 0, color: "var(--v5-text)" }}>
|
||||||
복사할 기준 데이터
|
복사할 기준 데이터
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ fontSize: "0.7rem", color: "var(--v5-text-sec)", marginTop: 4, maxWidth: 560 }}>
|
<div style={{ fontSize: "0.85rem", color: "var(--v5-text-sec)", marginTop: 6, maxWidth: 620, fontWeight: 500 }}>
|
||||||
기본 회사(INVION_DEFAULT) 의 기준 데이터 중 새 회사에 복제할 항목을 선택합니다.
|
기본 회사(INVION_DEFAULT) 의 기준 데이터 중 새 회사에 복제할 항목을 선택합니다.
|
||||||
<b style={{ color: "var(--v5-text)" }}> 필수 그룹</b>은 해제할 수 없습니다.
|
<b style={{ color: "var(--v5-text)" }}> 필수 그룹</b>은 해제할 수 없습니다.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div style={{ padding: "1rem", color: "var(--v5-text-muted)", fontSize: "0.68rem" }}>로딩 중...</div>
|
<div style={{ padding: "1.2rem", color: "var(--v5-text-muted)", fontSize: "0.82rem" }}>로딩 중...</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginTop: "1.1rem", display: "flex", flexDirection: "column", gap: "0.55rem" }}>
|
<div style={{ marginTop: "1.3rem", display: "flex", flexDirection: "column", gap: "0.7rem" }}>
|
||||||
{required.length > 0 && (
|
{required.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SubHead>필수 (해제 불가)</SubHead>
|
<SubHead>필수 (해제 불가)</SubHead>
|
||||||
@@ -141,24 +143,25 @@ export default function Step2Template({
|
|||||||
<div style={{ position: "sticky", top: 0, alignSelf: "start" }}>
|
<div style={{ position: "sticky", top: 0, alignSelf: "start" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "1rem",
|
padding: "1.15rem 1.2rem",
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
|
boxShadow: "0 0 12px rgba(var(--v5-cyan-rgb), 0.04)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.52rem",
|
fontSize: "0.68rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.18em",
|
letterSpacing: "0.2em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-cyan)",
|
color: "var(--v5-cyan)",
|
||||||
marginBottom: "0.7rem",
|
marginBottom: "0.85rem",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
SUMMARY
|
요약
|
||||||
</div>
|
</div>
|
||||||
<SummaryRow
|
<SummaryRow
|
||||||
label="선택된 그룹"
|
label="선택된 그룹"
|
||||||
@@ -210,31 +213,32 @@ function TemplateCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="wiz-tplcard"
|
||||||
style={{
|
style={{
|
||||||
border: `1px solid ${checked ? "rgba(var(--v5-cyan-rgb), 0.35)" : "var(--v5-border)"}`,
|
border: `1px solid ${checked ? "rgba(var(--v5-cyan-rgb), 0.4)" : "var(--v5-border)"}`,
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
background: checked ? "rgba(var(--v5-cyan-rgb), 0.04)" : "var(--v5-surface-solid)",
|
background: checked ? "rgba(var(--v5-cyan-rgb), 0.04)" : "var(--v5-surface-solid)",
|
||||||
transition: "all 0.2s",
|
transition: "transform 0.22s cubic-bezier(0.4,0,0.2,1), border-color 0.22s ease, background 0.22s ease, box-shadow 0.22s ease",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
boxShadow: checked ? "0 0 18px rgba(var(--v5-cyan-rgb), 0.08)" : "none",
|
boxShadow: checked ? "0 0 10px rgba(var(--v5-cyan-rgb), 0.07)" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => !locked && onToggle()}
|
onClick={() => !locked && onToggle()}
|
||||||
style={{
|
style={{
|
||||||
padding: "0.85rem 1rem",
|
padding: "1rem 1.15rem",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.7rem",
|
gap: "0.85rem",
|
||||||
cursor: locked ? "not-allowed" : "pointer",
|
cursor: locked ? "not-allowed" : "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* checkbox */}
|
{/* checkbox */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 18,
|
width: 22,
|
||||||
height: 18,
|
height: 22,
|
||||||
borderRadius: 4,
|
borderRadius: 5,
|
||||||
border: `1.5px solid ${checked ? "var(--v5-cyan)" : "var(--v5-border)"}`,
|
border: `1.5px solid ${checked ? "var(--v5-cyan)" : "var(--v5-border)"}`,
|
||||||
background: checked ? "var(--v5-cyan)" : "transparent",
|
background: checked ? "var(--v5-cyan)" : "transparent",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -242,43 +246,45 @@ function TemplateCard({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
boxShadow: checked ? "0 0 10px rgba(var(--v5-cyan-rgb), 0.4)" : "none",
|
boxShadow: checked ? "0 0 6px rgba(var(--v5-cyan-rgb), 0.25)" : "none",
|
||||||
opacity: locked ? 0.7 : 1,
|
opacity: locked ? 0.7 : 1,
|
||||||
|
transition: "all 0.2s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{checked && <Check size={12} strokeWidth={3} />}
|
{checked && <Check size={14} strokeWidth={3} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* icon tile */}
|
{/* icon tile */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 32,
|
width: 38,
|
||||||
height: 32,
|
height: 38,
|
||||||
borderRadius: 8,
|
borderRadius: 9,
|
||||||
background: checked ? "rgba(var(--v5-cyan-rgb), 0.12)" : "var(--v5-bg-subtle)",
|
background: checked ? "rgba(var(--v5-cyan-rgb), 0.12)" : "var(--v5-bg-subtle)",
|
||||||
color: checked ? "var(--v5-cyan)" : "var(--v5-text-muted)",
|
color: checked ? "var(--v5-cyan)" : "var(--v5-text-muted)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
transition: "all 0.22s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ic size={15} strokeWidth={1.75} />
|
<Ic size={18} strokeWidth={1.75} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* label */}
|
{/* label */}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 7 }}>
|
||||||
<span style={{ fontSize: "0.82rem", fontWeight: 700, color: "var(--v5-text)" }}>
|
<span style={{ fontSize: "0.95rem", fontWeight: 700, color: "var(--v5-text)", letterSpacing: "-0.01em" }}>
|
||||||
{g.label || g.id}
|
{g.label || g.id}
|
||||||
</span>
|
</span>
|
||||||
{locked && (
|
{locked && (
|
||||||
<span
|
<span
|
||||||
title="필수 템플릿입니다"
|
title="필수 템플릿입니다"
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.5rem",
|
fontSize: "0.62rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
padding: "0.1rem 0.4rem",
|
padding: "0.15rem 0.5rem",
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
background: "rgba(var(--v5-red-rgb), 0.1)",
|
background: "rgba(var(--v5-red-rgb), 0.1)",
|
||||||
color: "var(--v5-red)",
|
color: "var(--v5-red)",
|
||||||
@@ -288,16 +294,17 @@ function TemplateCard({
|
|||||||
gap: 3,
|
gap: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Lock size={8} /> 필수
|
<Lock size={10} /> 필수
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.6rem",
|
fontSize: "0.72rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
marginTop: 2,
|
marginTop: 3,
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{meta.join(" · ")}
|
{meta.join(" · ")}
|
||||||
@@ -311,53 +318,66 @@ function TemplateCard({
|
|||||||
onExpand();
|
onExpand();
|
||||||
}}
|
}}
|
||||||
aria-label={expanded ? "접기" : "펼치기"}
|
aria-label={expanded ? "접기" : "펼치기"}
|
||||||
|
className="wiz-chevron"
|
||||||
style={{
|
style={{
|
||||||
width: 24,
|
width: 30,
|
||||||
height: 24,
|
height: 30,
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expanded && tables.length > 0 && (
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
display: "grid",
|
||||||
padding: "0.7rem 1rem 0.85rem 3.4rem",
|
gridTemplateRows: expanded && tables.length > 0 ? "1fr" : "0fr",
|
||||||
borderTop: "1px solid var(--v5-border)",
|
transition: "grid-template-rows 0.3s cubic-bezier(0.4,0,0.2,1)",
|
||||||
background: "var(--v5-bg)",
|
}}
|
||||||
display: "flex",
|
>
|
||||||
flexWrap: "wrap",
|
<div style={{ overflow: "hidden" }}>
|
||||||
gap: 4,
|
{tables.length > 0 && (
|
||||||
}}
|
<div
|
||||||
>
|
|
||||||
{tables.map((t) => (
|
|
||||||
<span
|
|
||||||
key={t}
|
|
||||||
style={{
|
style={{
|
||||||
padding: "0.15rem 0.45rem",
|
padding: "0.8rem 1.15rem 1rem 3.85rem",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
borderTop: "1px solid var(--v5-border)",
|
||||||
fontSize: "0.58rem",
|
background: "var(--v5-bg)",
|
||||||
background: "var(--v5-surface-solid)",
|
display: "flex",
|
||||||
border: "1px solid var(--v5-border)",
|
flexWrap: "wrap",
|
||||||
borderRadius: 4,
|
gap: 5,
|
||||||
color: "var(--v5-text-sec)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t}
|
{tables.map((t) => (
|
||||||
</span>
|
<span
|
||||||
))}
|
key={t}
|
||||||
|
style={{
|
||||||
|
padding: "0.22rem 0.55rem",
|
||||||
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
fontSize: "0.72rem",
|
||||||
|
background: "var(--v5-surface-solid)",
|
||||||
|
border: "1px solid var(--v5-border)",
|
||||||
|
borderRadius: 5,
|
||||||
|
color: "var(--v5-text-sec)",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -366,12 +386,12 @@ function SubHead({ children, style }: { children: React.ReactNode; style?: React
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.52rem",
|
fontSize: "0.68rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.15em",
|
letterSpacing: "0.15em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
padding: "4px 0",
|
padding: "6px 0",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
@@ -400,14 +420,14 @@ function SummaryRow({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "baseline",
|
alignItems: "baseline",
|
||||||
padding: "0.45rem 0",
|
padding: "0.55rem 0",
|
||||||
borderBottom: isLast ? "none" : "1px solid var(--v5-border-subtle, rgba(0,0,0,0.05))",
|
borderBottom: isLast ? "none" : "1px solid var(--v5-border-subtle, rgba(0,0,0,0.06))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: "0.6rem", color: "var(--v5-text-muted)" }}>{label}</span>
|
<span style={{ fontSize: "0.78rem", color: "var(--v5-text-muted)", fontWeight: 500 }}>{label}</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.78rem",
|
fontSize: "0.92rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontFamily: mono ? "var(--v5-font-mono)" : "inherit",
|
fontFamily: mono ? "var(--v5-font-mono)" : "inherit",
|
||||||
color: accent === "cyan" ? "var(--v5-cyan)" : "var(--v5-text)",
|
color: accent === "cyan" ? "var(--v5-cyan)" : "var(--v5-text)",
|
||||||
|
|||||||
@@ -68,80 +68,88 @@ export default function Step3Admin({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const passwordActions = (
|
const passwordActions = (
|
||||||
<div style={{ display: "flex", gap: 4 }}>
|
<div style={{ display: "flex", gap: 5 }}>
|
||||||
<IconBtn title={visible ? "숨기기" : "보기"} onClick={() => setVisible((v) => !v)}>
|
<IconBtn title={visible ? "숨기기" : "보기"} onClick={() => setVisible((v) => !v)}>
|
||||||
{visible ? <EyeOff size={12} /> : <Eye size={12} />}
|
{visible ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
<IconBtn title="재생성" onClick={regen}>
|
<IconBtn title="재생성" onClick={regen}>
|
||||||
<RefreshCw size={12} />
|
<RefreshCw size={14} />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
<button
|
<button
|
||||||
onClick={copy}
|
onClick={copy}
|
||||||
title="복사"
|
title="복사"
|
||||||
|
className={`wiz-btn wiz-btn-${copied ? "primary" : "cyan"}`}
|
||||||
style={{
|
style={{
|
||||||
height: 30,
|
height: 36,
|
||||||
padding: "0 0.6rem",
|
padding: "0 0.85rem",
|
||||||
borderRadius: 7,
|
borderRadius: 8,
|
||||||
border: `1px solid ${copied ? "rgb(var(--v5-green-rgb))" : "var(--v5-cyan)"}`,
|
border: `1px solid ${copied ? "rgb(var(--v5-green-rgb))" : "var(--v5-cyan)"}`,
|
||||||
background: copied ? "rgb(var(--v5-green-rgb))" : "var(--v5-cyan)",
|
background: copied ? "rgb(var(--v5-green-rgb))" : "var(--v5-cyan)",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 5,
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.78rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
transition: "all 0.2s",
|
|
||||||
boxShadow: copied
|
boxShadow: copied
|
||||||
? "0 0 14px rgba(var(--v5-green-rgb), 0.4)"
|
? "0 0 8px rgba(var(--v5-green-rgb), 0.25)"
|
||||||
: "0 0 10px rgba(var(--v5-cyan-rgb), 0.3)",
|
: "0 0 6px rgba(var(--v5-cyan-rgb), 0.2)",
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{copied ? <Check size={11} /> : <Copy size={11} />}
|
{copied ? <Check size={13} /> : <Copy size={13} />}
|
||||||
{copied ? "복사됨" : "복사"}
|
{copied ? "복사됨" : "복사"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: "1.4rem 1.6rem", maxWidth: 820 }}>
|
<div
|
||||||
|
className="wiz-step"
|
||||||
|
style={{
|
||||||
|
padding: "1.6rem 1.8rem",
|
||||||
|
maxWidth: 920,
|
||||||
|
animation: "wizSlideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.55rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.15em",
|
letterSpacing: "0.18em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-cyan)",
|
color: "var(--v5-cyan)",
|
||||||
marginBottom: 6,
|
marginBottom: 8,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
03 · ADMIN ACCOUNT
|
03 · 관리자 계정
|
||||||
</div>
|
</div>
|
||||||
<h2 style={{ fontSize: "1.15rem", fontWeight: 800, letterSpacing: "-0.02em", margin: 0, color: "var(--v5-text)" }}>
|
<h2 style={{ fontSize: "1.4rem", fontWeight: 800, letterSpacing: "-0.02em", margin: 0, color: "var(--v5-text)" }}>
|
||||||
초기 관리자 계정
|
초기 관리자 계정
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ fontSize: "0.7rem", color: "var(--v5-text-sec)", marginTop: 4, maxWidth: 560 }}>
|
<div style={{ fontSize: "0.85rem", color: "var(--v5-text-sec)", marginTop: 6, maxWidth: 640, fontWeight: 500, lineHeight: 1.55 }}>
|
||||||
시스템이 자동으로{" "}
|
시스템이 자동으로{" "}
|
||||||
<code style={{ fontFamily: "var(--v5-font-mono)", color: "var(--v5-primary)" }}>COMPANY_ADMIN</code> 권한의 관리자 계정을
|
<code style={{ fontFamily: "var(--v5-font-mono)", color: "var(--v5-primary)", fontSize: "0.82rem" }}>COMPANY_ADMIN</code> 권한의 관리자 계정을
|
||||||
생성합니다. 생성된 비밀번호는{" "}
|
생성합니다. 생성된 비밀번호는{" "}
|
||||||
<b style={{ color: "var(--v5-text)" }}>이 화면을 벗어나면 다시 볼 수 없으며</b>, DB 에는 BCrypt 해시만 저장됩니다.
|
<b style={{ color: "var(--v5-text)" }}>이 화면을 벗어나면 다시 볼 수 없으며</b>, DB 에는 BCrypt 해시만 저장됩니다.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "1.2rem",
|
marginTop: "1.4rem",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
boxShadow: "0 0 12px rgba(var(--v5-cyan-rgb), 0.03)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Row
|
<Row
|
||||||
label="USER_ID"
|
label="USER_ID"
|
||||||
sub="자동 · 읽기전용"
|
sub="자동 · 읽기전용"
|
||||||
trailing={<Lock size={10} color="var(--v5-text-muted)" />}
|
trailing={<Lock size={12} color="var(--v5-text-muted)" />}
|
||||||
>
|
>
|
||||||
<ReadBox mono>{userId}</ReadBox>
|
<ReadBox mono>{userId}</ReadBox>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -149,21 +157,21 @@ export default function Step3Admin({
|
|||||||
<Row label="INITIAL_PASSWORD" sub="무작위 12자" trailing={passwordActions}>
|
<Row label="INITIAL_PASSWORD" sub="무작위 12자" trailing={passwordActions}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.45rem 0.6rem",
|
padding: "0.6rem 0.8rem",
|
||||||
background: "rgba(var(--v5-cyan-rgb), 0.05)",
|
background: "rgba(var(--v5-cyan-rgb), 0.05)",
|
||||||
border: "1px solid rgba(var(--v5-cyan-rgb), 0.25)",
|
border: "1px solid rgba(var(--v5-cyan-rgb), 0.28)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.82rem",
|
fontSize: "1rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "var(--v5-cyan)",
|
color: "var(--v5-cyan)",
|
||||||
letterSpacing: "0.02em",
|
letterSpacing: "0.03em",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 6,
|
gap: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KeyRound size={13} />
|
<KeyRound size={16} />
|
||||||
<span style={{ flex: 1 }}>{visible ? pw : "•".repeat(pw.length || 12)}</span>
|
<span style={{ flex: 1 }}>{visible ? pw : "•".repeat(pw.length || 12)}</span>
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -171,30 +179,30 @@ export default function Step3Admin({
|
|||||||
<Row label="USER_TYPE" sub="고정 · 수정 불가">
|
<Row label="USER_TYPE" sub="고정 · 수정 불가">
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
padding: "0.25rem 0.55rem",
|
padding: "0.32rem 0.7rem",
|
||||||
background: "rgba(var(--v5-primary-rgb), 0.1)",
|
background: "rgba(var(--v5-primary-rgb), 0.1)",
|
||||||
color: "var(--v5-primary)",
|
color: "var(--v5-primary)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.78rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Shield size={10} />
|
<Shield size={12} />
|
||||||
COMPANY_ADMIN
|
COMPANY_ADMIN
|
||||||
</span>
|
</span>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row label="FORCE_PW_CHANGE" sub="권장" last>
|
<Row label="FORCE_PW_CHANGE" sub="권장" last>
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
|
<label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }}>
|
||||||
<Switch
|
<Switch
|
||||||
on={!!state.force_password_change}
|
on={!!state.force_password_change}
|
||||||
onChange={() => setState({ force_password_change: !state.force_password_change })}
|
onChange={() => setState({ force_password_change: !state.force_password_change })}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: "0.7rem", color: "var(--v5-text)" }}>
|
<span style={{ fontSize: "0.85rem", color: "var(--v5-text)", fontWeight: 500 }}>
|
||||||
첫 로그인 시 비밀번호 변경 강제
|
첫 로그인 시 비밀번호 변경 강제
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -204,18 +212,18 @@ export default function Step3Admin({
|
|||||||
{/* 경고 배너 */}
|
{/* 경고 배너 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "1rem",
|
marginTop: "1.15rem",
|
||||||
padding: "0.75rem 0.9rem",
|
padding: "0.9rem 1.05rem",
|
||||||
background: "rgba(var(--v5-red-rgb), 0.04)",
|
background: "rgba(var(--v5-red-rgb), 0.04)",
|
||||||
border: "1px solid rgba(var(--v5-red-rgb), 0.25)",
|
border: "1px solid rgba(var(--v5-red-rgb), 0.25)",
|
||||||
borderRadius: 8,
|
borderRadius: 9,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
gap: 8,
|
gap: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AlertTriangle size={14} color="var(--v5-red)" style={{ marginTop: 2, flexShrink: 0 }} />
|
<AlertTriangle size={16} color="var(--v5-red)" style={{ marginTop: 2, flexShrink: 0 }} />
|
||||||
<div style={{ fontSize: "0.68rem", color: "var(--v5-text)", lineHeight: 1.5 }}>
|
<div style={{ fontSize: "0.82rem", color: "var(--v5-text)", lineHeight: 1.55, fontWeight: 500 }}>
|
||||||
<b>이 비밀번호는 다시 볼 수 없습니다.</b>
|
<b>이 비밀번호는 다시 볼 수 없습니다.</b>
|
||||||
<span style={{ color: "var(--v5-text-sec)" }}>
|
<span style={{ color: "var(--v5-text-sec)" }}>
|
||||||
{" "}
|
{" "}
|
||||||
@@ -243,27 +251,28 @@ function Row({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.9rem 1.1rem",
|
padding: "1.05rem 1.25rem",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "130px 1fr auto",
|
gridTemplateColumns: "160px 1fr auto",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "1rem",
|
gap: "1.15rem",
|
||||||
borderBottom: last ? "none" : "1px solid var(--v5-border)",
|
borderBottom: last ? "none" : "1px solid var(--v5-border)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.52rem",
|
fontSize: "0.68rem",
|
||||||
letterSpacing: "0.1em",
|
letterSpacing: "0.1em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
fontWeight: 700,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
{sub && <div style={{ fontSize: "0.55rem", color: "var(--v5-text-muted)", marginTop: 2 }}>{sub}</div>}
|
{sub && <div style={{ fontSize: "0.72rem", color: "var(--v5-text-muted)", marginTop: 3, fontWeight: 500 }}>{sub}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
<div>{trailing}</div>
|
<div>{trailing}</div>
|
||||||
@@ -284,10 +293,11 @@ function IconBtn({
|
|||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={title}
|
title={title}
|
||||||
|
className="wiz-iconbtn"
|
||||||
style={{
|
style={{
|
||||||
width: 30,
|
width: 36,
|
||||||
height: 30,
|
height: 36,
|
||||||
borderRadius: 7,
|
borderRadius: 8,
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
color: "var(--v5-text-sec)",
|
color: "var(--v5-text-sec)",
|
||||||
@@ -306,12 +316,13 @@ function ReadBox({ children, mono }: { children: React.ReactNode; mono?: boolean
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.45rem 0.6rem",
|
padding: "0.6rem 0.75rem",
|
||||||
background: "var(--v5-bg-subtle)",
|
background: "var(--v5-bg-subtle)",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
fontFamily: mono ? "var(--v5-font-mono)" : "inherit",
|
fontFamily: mono ? "var(--v5-font-mono)" : "inherit",
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.9rem",
|
||||||
|
fontWeight: 500,
|
||||||
color: "var(--v5-text)",
|
color: "var(--v5-text)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -325,14 +336,14 @@ function Switch({ on, onChange }: { on: boolean; onChange: () => void }) {
|
|||||||
<div
|
<div
|
||||||
onClick={onChange}
|
onClick={onChange}
|
||||||
style={{
|
style={{
|
||||||
width: 34,
|
width: 42,
|
||||||
height: 18,
|
height: 22,
|
||||||
borderRadius: 10,
|
borderRadius: 12,
|
||||||
background: on ? "var(--v5-cyan)" : "var(--v5-bg-subtle)",
|
background: on ? "var(--v5-cyan)" : "var(--v5-bg-subtle)",
|
||||||
border: `1px solid ${on ? "var(--v5-cyan)" : "var(--v5-border)"}`,
|
border: `1px solid ${on ? "var(--v5-cyan)" : "var(--v5-border)"}`,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
transition: "all 0.2s",
|
transition: "all 0.25s cubic-bezier(0.4,0,0.2,1)",
|
||||||
boxShadow: on ? "0 0 10px rgba(var(--v5-cyan-rgb), 0.3)" : "none",
|
boxShadow: on ? "0 0 6px rgba(var(--v5-cyan-rgb), 0.2)" : "none",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -340,13 +351,13 @@ function Switch({ on, onChange }: { on: boolean; onChange: () => void }) {
|
|||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 1,
|
top: 1,
|
||||||
left: on ? 17 : 1,
|
left: on ? 21 : 1,
|
||||||
width: 14,
|
width: 18,
|
||||||
height: 14,
|
height: 18,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
transition: "left 0.2s",
|
transition: "left 0.25s cubic-bezier(0.4,0,0.2,1)",
|
||||||
boxShadow: "0 1px 2px rgba(0,0,0,0.2)",
|
boxShadow: "0 1px 3px rgba(0,0,0,0.25)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Check, X, AlertOctagon } from "lucide-react";
|
import { Check, X, AlertOctagon, Rocket, Sparkles, Terminal, Zap } from "lucide-react";
|
||||||
import { createCompany, getProvisioningStatus, CreateCompanyRequest } from "@/lib/api/provisioning";
|
import { createCompany, getProvisioningStatus, CreateCompanyRequest } from "@/lib/api/provisioning";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 4 · 생성 진행 (시안 v2 포팅).
|
* Step 4 · 생성 진행 — 대대적 개편.
|
||||||
* 레이아웃: 좌측 (제목 + 큰 진행바 + 단계 리스트 + 결과 배너) + 우측 (다크 로그 콘솔)
|
* 좌측: 중앙 pulsing orb + 진행률 + 단계 타임라인 + 결과 배너
|
||||||
|
* 우측: 터미널 로그 (auto-scroll)
|
||||||
*
|
*
|
||||||
* 로직 (유지):
|
* 로직 (유지):
|
||||||
* - 마운트 즉시 POST /companies → jobId 취득 → 폴링 시작
|
* - 마운트 즉시 POST /companies → jobId 취득 → 폴링 시작
|
||||||
@@ -43,13 +44,19 @@ export default function Step4Run({
|
|||||||
const pollRef = useRef<NodeJS.Timeout | null>(null);
|
const pollRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const startedRef = useRef(false);
|
const startedRef = useRef(false);
|
||||||
const prevStepRef = useRef<string>("");
|
const prevStepRef = useRef<string>("");
|
||||||
|
const logScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 로그 append 헬퍼
|
|
||||||
function appendLog(line: string) {
|
function appendLog(line: string) {
|
||||||
const hhmmss = new Date().toTimeString().slice(0, 8);
|
const hhmmss = new Date().toTimeString().slice(0, 8);
|
||||||
setLog((lg) => [...lg, `[${hhmmss}] ${line}`]);
|
setLog((lg) => [...lg, `[${hhmmss}] ${line}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 로그 auto-scroll
|
||||||
|
useEffect(() => {
|
||||||
|
const el = logScrollRef.current;
|
||||||
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
|
}, [log]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (startedRef.current) return;
|
if (startedRef.current) return;
|
||||||
startedRef.current = true;
|
startedRef.current = true;
|
||||||
@@ -98,7 +105,6 @@ export default function Step4Run({
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setStatus(s);
|
setStatus(s);
|
||||||
|
|
||||||
// step 변화 감지 → 로그
|
|
||||||
if (s.currentStep && s.currentStep !== prevStepRef.current) {
|
if (s.currentStep && s.currentStep !== prevStepRef.current) {
|
||||||
const disp = DISPLAY_STEPS.find((d) => d.key === s.currentStep);
|
const disp = DISPLAY_STEPS.find((d) => d.key === s.currentStep);
|
||||||
appendLog(`▸ ${s.currentStep}${disp ? ` · ${disp.label}` : ""}`);
|
appendLog(`▸ ${s.currentStep}${disp ? ` · ${disp.label}` : ""}`);
|
||||||
@@ -155,36 +161,97 @@ export default function Step4Run({
|
|||||||
const elapsedSec = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
const elapsedSec = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
||||||
|
|
||||||
const variant: "running" | "success" | "failed" = failed ? "failed" : done ? "success" : "running";
|
const variant: "running" | "success" | "failed" = failed ? "failed" : done ? "success" : "running";
|
||||||
const barLabel = variant === "success" ? "COMPLETED" : variant === "failed" ? "HALTED" : "IN PROGRESS";
|
const barLabel = variant === "success" ? "완료" : variant === "failed" ? "중단" : "진행 중";
|
||||||
const accent =
|
const accent =
|
||||||
variant === "success" ? "rgb(var(--v5-green-rgb))" : variant === "failed" ? "var(--v5-red)" : "var(--v5-cyan)";
|
variant === "success" ? "rgb(var(--v5-green-rgb))" : variant === "failed" ? "var(--v5-red)" : "var(--v5-cyan)";
|
||||||
|
const accentRgb =
|
||||||
|
variant === "success" ? "var(--v5-green-rgb)" : variant === "failed" ? "var(--v5-red-rgb)" : "var(--v5-cyan-rgb)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="wiz-step"
|
||||||
style={{
|
style={{
|
||||||
padding: "1.4rem 1.6rem",
|
padding: "1.6rem 1.8rem",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 320px",
|
gridTemplateColumns: "1fr 360px",
|
||||||
gap: "1.6rem",
|
gap: "1.8rem",
|
||||||
|
animation: "wizSlideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<style>{`
|
||||||
|
@keyframes s4Orb {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
@keyframes s4OrbGlow {
|
||||||
|
0%, 100% { opacity: 0.7; transform: scale(1); }
|
||||||
|
50% { opacity: 1; transform: scale(1.15); }
|
||||||
|
}
|
||||||
|
@keyframes s4OrbRing1 {
|
||||||
|
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||||
|
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes s4OrbRing2 {
|
||||||
|
0% { transform: translate(-50%, -50%) rotate(360deg); }
|
||||||
|
100% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||||
|
}
|
||||||
|
@keyframes s4StepIn {
|
||||||
|
from { opacity: 0; transform: translateX(-8px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
@keyframes s4SuccessBurst {
|
||||||
|
0% { transform: scale(0.2); opacity: 0; }
|
||||||
|
60% { transform: scale(1.2); opacity: 1; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes s4SuccessRay {
|
||||||
|
0% { opacity: 0; transform: scaleY(0); }
|
||||||
|
50% { opacity: 0.9; transform: scaleY(1); }
|
||||||
|
100% { opacity: 0; transform: scaleY(1); }
|
||||||
|
}
|
||||||
|
@keyframes s4Shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
20% { transform: translateX(-4px); }
|
||||||
|
40% { transform: translateX(4px); }
|
||||||
|
60% { transform: translateX(-3px); }
|
||||||
|
80% { transform: translateX(3px); }
|
||||||
|
}
|
||||||
|
@keyframes s4Sweep {
|
||||||
|
0% { left: -40%; }
|
||||||
|
100% { left: 100%; }
|
||||||
|
}
|
||||||
|
@keyframes s4LogLine {
|
||||||
|
from { opacity: 0; transform: translateY(-2px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.s4-step-row {
|
||||||
|
animation: s4StepIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
.s4-log-line {
|
||||||
|
animation: s4LogLine 0.2s ease-out both;
|
||||||
|
}
|
||||||
|
.s4-shake {
|
||||||
|
animation: s4Shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.55rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.15em",
|
letterSpacing: "0.18em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-cyan)",
|
color: accent,
|
||||||
marginBottom: 6,
|
marginBottom: 8,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
04 · PROVISIONING
|
04 · 생성 진행
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
style={{
|
style={{
|
||||||
fontSize: "1.15rem",
|
fontSize: "1.4rem",
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
letterSpacing: "-0.02em",
|
letterSpacing: "-0.02em",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@@ -193,7 +260,7 @@ export default function Step4Run({
|
|||||||
>
|
>
|
||||||
{variant === "success" ? "회사 생성 완료" : variant === "failed" ? "생성 실패" : "회사 생성 중"}
|
{variant === "success" ? "회사 생성 완료" : variant === "failed" ? "생성 실패" : "회사 생성 중"}
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ fontSize: "0.7rem", color: "var(--v5-text-sec)", marginTop: 4 }}>
|
<div style={{ fontSize: "0.85rem", color: "var(--v5-text-sec)", marginTop: 6, fontWeight: 500, lineHeight: 1.55 }}>
|
||||||
{variant === "success" && (
|
{variant === "success" && (
|
||||||
<>
|
<>
|
||||||
<b style={{ color: "rgb(var(--v5-green-rgb))" }}>
|
<b style={{ color: "rgb(var(--v5-green-rgb))" }}>
|
||||||
@@ -206,22 +273,134 @@ export default function Step4Run({
|
|||||||
{variant === "running" && <>브라우저를 닫아도 서버에서 계속 진행됩니다.</>}
|
{variant === "running" && <>브라우저를 닫아도 서버에서 계속 진행됩니다.</>}
|
||||||
</div>
|
</div>
|
||||||
{createError && (
|
{createError && (
|
||||||
<div style={{ marginTop: 6, fontSize: "0.66rem", color: "var(--v5-red)", fontFamily: "var(--v5-font-mono)" }}>
|
<div style={{ marginTop: 8, fontSize: "0.78rem", color: "var(--v5-red)", fontFamily: "var(--v5-font-mono)" }}>
|
||||||
{createError}
|
{createError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 큰 진행 바 */}
|
{/* ── 중앙 Orb + 큰 진행 바 ── */}
|
||||||
<div
|
<div
|
||||||
|
className={variant === "failed" ? "s4-shake" : ""}
|
||||||
style={{
|
style={{
|
||||||
marginTop: "1.2rem",
|
marginTop: "1.3rem",
|
||||||
padding: "1.05rem 1.1rem",
|
padding: "1.6rem 1.4rem 1.25rem",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 10,
|
borderRadius: 14,
|
||||||
background: "var(--v5-surface-solid)",
|
background:
|
||||||
boxShadow: variant === "running" ? "0 0 26px rgba(var(--v5-cyan-rgb), 0.1)" : "none",
|
variant === "running"
|
||||||
|
? "linear-gradient(180deg, rgba(var(--v5-cyan-rgb), 0.04), var(--v5-surface-solid))"
|
||||||
|
: variant === "success"
|
||||||
|
? "linear-gradient(180deg, rgba(var(--v5-green-rgb), 0.05), var(--v5-surface-solid))"
|
||||||
|
: "linear-gradient(180deg, rgba(var(--v5-red-rgb), 0.04), var(--v5-surface-solid))",
|
||||||
|
boxShadow: `0 0 22px rgba(${accentRgb}, 0.08)`,
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Orb */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
height: 140,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: "1.1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* outer glow */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
borderRadius: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
background: `radial-gradient(circle, rgba(${accentRgb}, 0.14) 0%, rgba(${accentRgb}, 0) 70%)`,
|
||||||
|
animation: variant === "running" ? "s4OrbGlow 2.4s ease-in-out infinite" : "none",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* rotating ring (single solid arc) */}
|
||||||
|
{variant === "running" && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
width: 110,
|
||||||
|
height: 110,
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: "1.5px solid rgba(var(--v5-cyan-rgb), 0.12)",
|
||||||
|
borderTopColor: "rgba(var(--v5-cyan-rgb), 0.85)",
|
||||||
|
animation: "s4OrbRing2 2.4s linear infinite",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* success rays */}
|
||||||
|
{variant === "success" &&
|
||||||
|
Array.from({ length: 8 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
width: 2,
|
||||||
|
height: 46,
|
||||||
|
marginLeft: -1,
|
||||||
|
marginTop: -46,
|
||||||
|
background: "linear-gradient(to top, rgba(var(--v5-green-rgb), 0.7), transparent)",
|
||||||
|
transformOrigin: "bottom center",
|
||||||
|
transform: `rotate(${i * 45}deg)`,
|
||||||
|
animation: `s4SuccessRay 1.2s ease-out ${0.15 + i * 0.04}s both`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* core */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: 78,
|
||||||
|
height: 78,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background:
|
||||||
|
variant === "success"
|
||||||
|
? "linear-gradient(135deg, rgb(var(--v5-green-rgb)), #5de6b5)"
|
||||||
|
: variant === "failed"
|
||||||
|
? "linear-gradient(135deg, var(--v5-red), #ff6b6b)"
|
||||||
|
: "linear-gradient(135deg, var(--v5-cyan), var(--v5-primary))",
|
||||||
|
boxShadow: `0 0 18px rgba(${accentRgb}, 0.4), inset 0 -4px 10px rgba(0, 0, 0, 0.18)`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#fff",
|
||||||
|
animation:
|
||||||
|
variant === "running"
|
||||||
|
? "s4Orb 2.4s ease-in-out infinite"
|
||||||
|
: variant === "success"
|
||||||
|
? "s4SuccessBurst 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both"
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{variant === "success" ? (
|
||||||
|
<Check size={36} strokeWidth={2.5} />
|
||||||
|
) : variant === "failed" ? (
|
||||||
|
<X size={36} strokeWidth={2.5} />
|
||||||
|
) : (
|
||||||
|
<Rocket size={30} strokeWidth={1.75} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 진행 바 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -232,36 +411,53 @@ export default function Step4Run({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.56rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.12em",
|
letterSpacing: "0.15em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{variant === "running" && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: accent,
|
||||||
|
boxShadow: `0 0 4px ${accent}`,
|
||||||
|
animation: "pulsedot 1.4s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{barLabel}
|
{barLabel}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "1.3rem",
|
fontSize: "1.75rem",
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
letterSpacing: "-0.03em",
|
letterSpacing: "-0.03em",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
color: accent,
|
color: accent,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{progress}
|
{progress}
|
||||||
<span style={{ fontSize: "0.75rem", color: "var(--v5-text-muted)" }}>%</span>
|
<span style={{ fontSize: "0.9rem", color: "var(--v5-text-muted)", marginLeft: 1 }}>%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 6,
|
height: 8,
|
||||||
borderRadius: 3,
|
borderRadius: 4,
|
||||||
background: "var(--v5-bg-subtle)",
|
background: "var(--v5-bg-subtle)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
border: "1px solid var(--v5-border)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -270,12 +466,13 @@ export default function Step4Run({
|
|||||||
width: `${progress}%`,
|
width: `${progress}%`,
|
||||||
background:
|
background:
|
||||||
variant === "success"
|
variant === "success"
|
||||||
? "linear-gradient(90deg, rgb(var(--v5-green-rgb)), rgb(var(--v5-green-rgb)))"
|
? "linear-gradient(90deg, rgb(var(--v5-green-rgb)), #5de6b5)"
|
||||||
: variant === "failed"
|
: variant === "failed"
|
||||||
? "linear-gradient(90deg, var(--v5-red), #ff6b6b)"
|
? "linear-gradient(90deg, var(--v5-red), #ff6b6b)"
|
||||||
: "linear-gradient(90deg, var(--v5-cyan), var(--v5-primary))",
|
: "linear-gradient(90deg, var(--v5-cyan), var(--v5-primary))",
|
||||||
boxShadow: variant === "running" ? "0 0 12px rgba(var(--v5-cyan-rgb), 0.5)" : "none",
|
boxShadow: `0 0 8px rgba(${accentRgb}, 0.35)`,
|
||||||
transition: "width 0.4s",
|
transition: "width 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{variant === "running" && (
|
{variant === "running" && (
|
||||||
@@ -284,25 +481,26 @@ export default function Step4Run({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: 40,
|
width: "40%",
|
||||||
background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent)",
|
background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.45), transparent)",
|
||||||
left: `${Math.max(0, progress - 8)}%`,
|
animation: "s4Sweep 1.6s ease-in-out infinite",
|
||||||
animation: "shimmer 1.5s ease-in-out infinite",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "0.55rem",
|
marginTop: "0.7rem",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.55rem",
|
fontSize: "0.72rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>PROV_ID · {jobId || "—"}</span>
|
<span>작업번호 · {jobId || "—"}</span>
|
||||||
<span>
|
<span>
|
||||||
경과 {elapsedSec}s
|
경과 {elapsedSec}s
|
||||||
{variant === "running" && " · 진행 중"}
|
{variant === "running" && " · 진행 중"}
|
||||||
@@ -312,13 +510,13 @@ export default function Step4Run({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 단계 리스트 */}
|
{/* 단계 타임라인 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "1rem",
|
marginTop: "1.1rem",
|
||||||
padding: "0.4rem 1.1rem",
|
padding: "0.5rem 1.2rem",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -338,41 +536,42 @@ export default function Step4Run({
|
|||||||
{failed && (
|
{failed && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "1rem",
|
marginTop: "1.1rem",
|
||||||
padding: "0.85rem 1rem",
|
padding: "1rem 1.15rem",
|
||||||
border: "1px solid rgba(var(--v5-red-rgb), 0.3)",
|
border: "1px solid rgba(var(--v5-red-rgb), 0.35)",
|
||||||
background: "rgba(var(--v5-red-rgb), 0.04)",
|
background: "rgba(var(--v5-red-rgb), 0.05)",
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.6rem",
|
fontSize: "0.75rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "var(--v5-red)",
|
color: "var(--v5-red)",
|
||||||
letterSpacing: "0.12em",
|
letterSpacing: "0.12em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
marginBottom: 6,
|
marginBottom: 8,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 6,
|
gap: 7,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AlertOctagon size={12} /> ERROR · {status?.failedStep || "—"}
|
<AlertOctagon size={14} /> 오류 · {status?.failedStep || "—"}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.65rem",
|
fontSize: "0.82rem",
|
||||||
padding: "0.55rem 0.65rem",
|
padding: "0.7rem 0.8rem",
|
||||||
background: "var(--v5-bg)",
|
background: "var(--v5-bg)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
color: "var(--v5-text)",
|
color: "var(--v5-text)",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
overflowX: "auto",
|
overflowX: "auto",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{status?.errorMessage || createError || "—"}
|
{status?.errorMessage || createError || "—"}
|
||||||
@@ -384,46 +583,46 @@ export default function Step4Run({
|
|||||||
{done && (
|
{done && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "1rem",
|
marginTop: "1.1rem",
|
||||||
padding: "1rem 1.1rem",
|
padding: "1.15rem 1.3rem",
|
||||||
border: "1px solid rgba(var(--v5-green-rgb), 0.3)",
|
border: "1px solid rgba(var(--v5-green-rgb), 0.4)",
|
||||||
background: "rgba(var(--v5-green-rgb), 0.04)",
|
background: "linear-gradient(135deg, rgba(var(--v5-green-rgb), 0.08), rgba(var(--v5-green-rgb), 0.03))",
|
||||||
borderRadius: 10,
|
borderRadius: 12,
|
||||||
boxShadow: "0 0 24px rgba(var(--v5-green-rgb), 0.1)",
|
boxShadow: "0 0 16px rgba(var(--v5-green-rgb), 0.08)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.85rem",
|
gap: "1rem",
|
||||||
|
animation: "s4SuccessBurst 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both 0.2s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 40,
|
width: 48,
|
||||||
height: 40,
|
height: 48,
|
||||||
borderRadius: 10,
|
borderRadius: 12,
|
||||||
background: "linear-gradient(135deg, rgb(var(--v5-green-rgb)), rgb(var(--v5-green-rgb)))",
|
background: "linear-gradient(135deg, rgb(var(--v5-green-rgb)), #5de6b5)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
boxShadow: "0 0 14px rgba(var(--v5-green-rgb), 0.5)",
|
boxShadow: "0 0 12px rgba(var(--v5-green-rgb), 0.3)",
|
||||||
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check size={20} />
|
<Sparkles size={22} strokeWidth={1.75} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.56rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "rgb(var(--v5-green-rgb))",
|
color: "rgb(var(--v5-green-rgb))",
|
||||||
letterSpacing: "0.15em",
|
letterSpacing: "0.08em",
|
||||||
textTransform: "uppercase",
|
|
||||||
fontFamily: "var(--v5-font-mono)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
READY
|
접속 준비 완료
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: "0.85rem", fontWeight: 700, marginTop: 2, color: "var(--v5-text)" }}>
|
<div style={{ fontSize: "1rem", fontWeight: 700, marginTop: 3, color: "var(--v5-text)" }}>
|
||||||
<span style={{ fontFamily: "var(--v5-font-mono)", color: "var(--v5-cyan)" }}>
|
<span style={{ fontFamily: "var(--v5-font-mono)", color: "var(--v5-cyan)" }}>
|
||||||
{state.subdomain}.invyone.com
|
{state.subdomain}.invyone.com
|
||||||
</span>
|
</span>
|
||||||
@@ -431,10 +630,11 @@ export default function Step4Run({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.6rem",
|
fontSize: "0.78rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
marginTop: 2,
|
marginTop: 3,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
로그인: {state.db_prefix}_admin / [Step 3 에서 복사한 비밀번호]
|
로그인: {state.db_prefix}_admin / [Step 3 에서 복사한 비밀번호]
|
||||||
@@ -444,54 +644,74 @@ export default function Step4Run({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측: 로그 콘솔 */}
|
{/* ── 우측: 터미널 로그 콘솔 ── */}
|
||||||
<div style={{ alignSelf: "start" }}>
|
<div style={{ alignSelf: "start" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "#0a0a0c",
|
background: "#0a0a0c",
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
boxShadow: "0 0 16px rgba(0, 0, 0, 0.25)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.55rem 0.7rem",
|
padding: "0.75rem 0.95rem",
|
||||||
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 6,
|
gap: 8,
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.56rem",
|
fontSize: "0.7rem",
|
||||||
letterSpacing: "0.12em",
|
letterSpacing: "0.15em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
color: "#a8a8b0",
|
color: "#a8a8b0",
|
||||||
|
fontWeight: 600,
|
||||||
|
background: "linear-gradient(180deg, #111114, #0a0a0c)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Terminal size={13} />
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
width: 7,
|
width: 7,
|
||||||
height: 7,
|
height: 7,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
background: accent,
|
background: accent,
|
||||||
boxShadow: `0 0 6px ${accent}`,
|
boxShadow: `0 0 8px ${accent}`,
|
||||||
|
animation: variant === "running" ? "pulsedot 1.4s ease-in-out infinite" : "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
PROVISIONING LOG
|
생성 로그
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<span style={{ fontSize: "0.62rem", color: "#6a6a72", letterSpacing: "0.08em" }}>
|
||||||
|
{log.length} lines
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
ref={logScrollRef}
|
||||||
style={{
|
style={{
|
||||||
padding: "0.55rem 0.7rem",
|
padding: "0.75rem 0.95rem",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.72rem",
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.7,
|
||||||
color: "#d4d4dc",
|
color: "#d4d4dc",
|
||||||
maxHeight: 420,
|
maxHeight: 480,
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
|
background:
|
||||||
|
"radial-gradient(ellipse at top, rgba(var(--v5-cyan-rgb), 0.04), transparent 60%), #0a0a0c",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{log.length === 0 ? <span style={{ color: "#70707a" }}>(로그 없음)</span> : log.join("\n")}
|
{log.length === 0 ? (
|
||||||
|
<span style={{ color: "#70707a" }}>(로그 없음)</span>
|
||||||
|
) : (
|
||||||
|
log.map((l, i) => (
|
||||||
|
<div key={i} className="s4-log-line" style={{ color: colorizeLog(l) }}>
|
||||||
|
{l}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -499,6 +719,13 @@ export default function Step4Run({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function colorizeLog(line: string): string {
|
||||||
|
if (line.includes("✓") || line.includes("━ READY")) return "#5de6b5";
|
||||||
|
if (line.includes("✗") || line.includes("FAILED")) return "#ff8a8a";
|
||||||
|
if (line.includes("▸")) return "#8ce7ff";
|
||||||
|
return "#d4d4dc";
|
||||||
|
}
|
||||||
|
|
||||||
function RunRow({
|
function RunRow({
|
||||||
step,
|
step,
|
||||||
idx,
|
idx,
|
||||||
@@ -526,20 +753,22 @@ function RunRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="s4-step-row"
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "32px 1fr 90px",
|
gridTemplateColumns: "40px 1fr 100px",
|
||||||
gap: "0.85rem",
|
gap: "1rem",
|
||||||
padding: "0.7rem 0",
|
padding: "0.85rem 0",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
animationDelay: `${idx * 60}ms`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* circle + connecting line */}
|
{/* circle + connecting line */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
height: 32,
|
height: 38,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
@@ -548,19 +777,19 @@ function RunRow({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 26,
|
top: 32,
|
||||||
bottom: -18,
|
bottom: -18,
|
||||||
width: 2,
|
width: 2,
|
||||||
background: isDone ? "rgb(var(--v5-green-rgb))" : "var(--v5-border)",
|
background: isDone ? "rgb(var(--v5-green-rgb))" : "var(--v5-border)",
|
||||||
left: 15,
|
left: 19,
|
||||||
transition: "background 0.3s",
|
transition: "background 0.4s ease",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 28,
|
width: 34,
|
||||||
height: 28,
|
height: 34,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
background: isFailed
|
background: isFailed
|
||||||
? "var(--v5-red)"
|
? "var(--v5-red)"
|
||||||
@@ -573,26 +802,29 @@ function RunRow({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
color: "#fff",
|
color: isPending ? "var(--v5-text-muted)" : "#fff",
|
||||||
boxShadow: isActive
|
boxShadow: isActive
|
||||||
? "0 0 14px rgba(var(--v5-cyan-rgb), 0.5)"
|
? "0 0 10px rgba(var(--v5-cyan-rgb), 0.35)"
|
||||||
: isFailed
|
: isFailed
|
||||||
? "0 0 14px rgba(var(--v5-red-rgb), 0.4)"
|
? "0 0 10px rgba(var(--v5-red-rgb), 0.3)"
|
||||||
: "none",
|
: isDone
|
||||||
transition: "all 0.3s",
|
? "0 0 6px rgba(var(--v5-green-rgb), 0.25)"
|
||||||
|
: "none",
|
||||||
|
transition: "all 0.35s cubic-bezier(0.4,0,0.2,1)",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
animation: isActive ? "wizPulseRing 1.8s ease-out infinite" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDone && <Check size={13} />}
|
{isDone && <Check size={15} strokeWidth={2.5} />}
|
||||||
{isFailed && <X size={13} />}
|
{isFailed && <X size={15} strokeWidth={2.5} />}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
width: 10,
|
width: 13,
|
||||||
height: 10,
|
height: 13,
|
||||||
border: "2px solid rgba(255,255,255,0.35)",
|
border: "2.5px solid rgba(255,255,255,0.35)",
|
||||||
borderTopColor: "#fff",
|
borderTopColor: "#fff",
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
}}
|
}}
|
||||||
@@ -602,7 +834,7 @@ function RunRow({
|
|||||||
{isPending && (
|
{isPending && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.6rem",
|
fontSize: "0.78rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
@@ -617,8 +849,9 @@ function RunRow({
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.78rem",
|
fontSize: "0.92rem",
|
||||||
fontWeight: isActive ? 700 : 600,
|
fontWeight: isActive ? 700 : 600,
|
||||||
|
letterSpacing: "-0.01em",
|
||||||
color: isFailed
|
color: isFailed
|
||||||
? "var(--v5-red)"
|
? "var(--v5-red)"
|
||||||
: isActive
|
: isActive
|
||||||
@@ -626,16 +859,27 @@ function RunRow({
|
|||||||
: isDone
|
: isDone
|
||||||
? "var(--v5-text)"
|
? "var(--v5-text)"
|
||||||
: "var(--v5-text-muted)",
|
: "var(--v5-text-muted)",
|
||||||
|
transition: "color 0.3s ease",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{step.label}
|
{step.label}
|
||||||
|
{isActive && (
|
||||||
|
<Zap
|
||||||
|
size={12}
|
||||||
|
style={{ color: "var(--v5-cyan)", animation: "pulsedot 1.4s ease-in-out infinite" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.6rem",
|
fontSize: "0.72rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
marginTop: 2,
|
marginTop: 3,
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{step.sub.replace("%db%", dbName)}
|
{step.sub.replace("%db%", dbName)}
|
||||||
@@ -645,8 +889,9 @@ function RunRow({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.76rem",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
fontWeight: 600,
|
||||||
color: isFailed
|
color: isFailed
|
||||||
? "var(--v5-red)"
|
? "var(--v5-red)"
|
||||||
: isDone
|
: isDone
|
||||||
@@ -654,6 +899,7 @@ function RunRow({
|
|||||||
: isActive
|
: isActive
|
||||||
? "var(--v5-cyan)"
|
? "var(--v5-cyan)"
|
||||||
: "var(--v5-text-muted)",
|
: "var(--v5-text-muted)",
|
||||||
|
transition: "color 0.3s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDone && "완료"}
|
{isDone && "완료"}
|
||||||
|
|||||||
@@ -29,19 +29,19 @@ export default function StepIndicator({ current }: { current: number }) {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.65rem",
|
gap: "0.85rem",
|
||||||
padding: "0.75rem 1rem",
|
padding: "0.95rem 1.2rem",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
background: active ? "rgba(var(--v5-cyan-rgb), 0.06)" : "transparent",
|
background: active ? "rgba(var(--v5-cyan-rgb), 0.06)" : "transparent",
|
||||||
borderRight: i < WIZARD_STEPS.length - 1 ? "1px solid var(--v5-border)" : "none",
|
borderRight: i < WIZARD_STEPS.length - 1 ? "1px solid var(--v5-border)" : "none",
|
||||||
transition: "background 0.25s",
|
transition: "background 0.3s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 26,
|
width: 34,
|
||||||
height: 26,
|
height: 34,
|
||||||
borderRadius: 7,
|
borderRadius: 9,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@@ -52,19 +52,20 @@ export default function StepIndicator({ current }: { current: number }) {
|
|||||||
: "var(--v5-bg-subtle)",
|
: "var(--v5-bg-subtle)",
|
||||||
color: done || active ? "#fff" : "var(--v5-text-muted)",
|
color: done || active ? "#fff" : "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.82rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
boxShadow: active ? "0 0 14px rgba(var(--v5-cyan-rgb), 0.5)" : "none",
|
boxShadow: active ? "0 0 10px rgba(var(--v5-cyan-rgb), 0.3)" : "none",
|
||||||
transition: "all 0.25s",
|
transition: "all 0.3s cubic-bezier(0.4,0,0.2,1)",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
animation: active ? "wizPulseRing 2s ease-out infinite" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{done ? <Check size={13} strokeWidth={2.5} /> : s.n}
|
{done ? <Check size={17} strokeWidth={2.5} /> : s.n}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ minWidth: 0, flex: 1 }}>
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.52rem",
|
fontSize: "0.65rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.15em",
|
letterSpacing: "0.15em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
@@ -76,15 +77,16 @@ export default function StepIndicator({ current }: { current: number }) {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.95rem",
|
||||||
fontWeight: active || done ? 700 : 500,
|
fontWeight: active || done ? 700 : 500,
|
||||||
color: active
|
color: active
|
||||||
? "var(--v5-cyan)"
|
? "var(--v5-cyan)"
|
||||||
: done
|
: done
|
||||||
? "var(--v5-text)"
|
? "var(--v5-text)"
|
||||||
: "var(--v5-text-sec)",
|
: "var(--v5-text-sec)",
|
||||||
marginTop: 1,
|
marginTop: 2,
|
||||||
letterSpacing: "-0.01em",
|
letterSpacing: "-0.01em",
|
||||||
|
transition: "color 0.3s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{s.label}
|
{s.label}
|
||||||
|
|||||||
@@ -83,12 +83,12 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) tryClose();
|
if (e.target === e.currentTarget) tryClose();
|
||||||
}}
|
}}
|
||||||
|
className="wiz-overlay"
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
background: "rgba(6, 5, 14, 0.5)",
|
background: "rgba(6, 5, 14, 0.55)",
|
||||||
backdropFilter: "blur(2px)",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@@ -102,20 +102,101 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
0% { transform: translateX(-40px); }
|
0% { transform: translateX(-40px); }
|
||||||
100% { transform: translateX(calc(100% + 40px)); }
|
100% { transform: translateX(calc(100% + 40px)); }
|
||||||
}
|
}
|
||||||
|
@keyframes wizOverlayIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes wizModalIn {
|
||||||
|
from { opacity: 0; transform: translateY(12px) scale(0.97); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
@keyframes wizSlideUp {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes wizFadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes wizPulseRing {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(var(--v5-cyan-rgb), 0.25); }
|
||||||
|
70% { box-shadow: 0 0 0 10px rgba(var(--v5-cyan-rgb), 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(var(--v5-cyan-rgb), 0); }
|
||||||
|
}
|
||||||
|
@keyframes wizOrbitSpin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes wizSweep {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
@keyframes wizSuccessPop {
|
||||||
|
0% { transform: scale(0.4); opacity: 0; }
|
||||||
|
60% { transform: scale(1.15); opacity: 1; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes wizShake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
20% { transform: translateX(-5px); }
|
||||||
|
40% { transform: translateX(5px); }
|
||||||
|
60% { transform: translateX(-3px); }
|
||||||
|
80% { transform: translateX(3px); }
|
||||||
|
}
|
||||||
|
@keyframes wizLogAppear {
|
||||||
|
from { opacity: 0; transform: translateY(-3px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.wiz-overlay { animation: wizOverlayIn 0.18s ease-out; }
|
||||||
|
.wiz-modal { animation: wizModalIn 0.28s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||||
|
.wiz-btn {
|
||||||
|
transition: transform 0.18s cubic-bezier(0.4,0,0.2,1),
|
||||||
|
box-shadow 0.18s ease,
|
||||||
|
background 0.18s ease,
|
||||||
|
border-color 0.18s ease,
|
||||||
|
opacity 0.18s ease;
|
||||||
|
}
|
||||||
|
.wiz-btn:not(:disabled):hover { transform: translateY(-1px); }
|
||||||
|
.wiz-btn:not(:disabled):active { transform: translateY(0) scale(0.97); }
|
||||||
|
.wiz-btn-cyan:not(:disabled):hover {
|
||||||
|
box-shadow: 0 0 14px rgba(var(--v5-cyan-rgb), 0.3) !important;
|
||||||
|
}
|
||||||
|
.wiz-btn-primary:not(:disabled):hover {
|
||||||
|
box-shadow: 0 0 14px rgba(var(--v5-primary-rgb), 0.28) !important;
|
||||||
|
}
|
||||||
|
.wiz-btn-danger:not(:disabled):hover {
|
||||||
|
box-shadow: 0 0 14px rgba(var(--v5-red-rgb), 0.28) !important;
|
||||||
|
}
|
||||||
|
.wiz-btn-secondary:not(:disabled):hover {
|
||||||
|
background: var(--v5-surface-hover) !important;
|
||||||
|
border-color: var(--v5-primary) !important;
|
||||||
|
}
|
||||||
|
.wiz-btn-ghost:not(:disabled):hover {
|
||||||
|
color: var(--v5-text) !important;
|
||||||
|
background: var(--v5-surface-hover) !important;
|
||||||
|
}
|
||||||
|
.wiz-iconbtn {
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
}
|
||||||
|
.wiz-iconbtn:hover {
|
||||||
|
background: var(--v5-surface-hover) !important;
|
||||||
|
color: var(--v5-text) !important;
|
||||||
|
border-color: var(--v5-primary) !important;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
className="wiz-modal"
|
||||||
style={{
|
style={{
|
||||||
width: "min(1180px, 100%)",
|
width: "min(1260px, 100%)",
|
||||||
height: "min(820px, 100%)",
|
height: "min(880px, 100%)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
background: "var(--v5-bg)",
|
background: "var(--v5-bg)",
|
||||||
borderRadius: 12,
|
borderRadius: 14,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
boxShadow: "0 0 60px rgba(var(--v5-cyan-rgb), 0.14)",
|
boxShadow: "0 10px 40px rgba(0, 0, 0, 0.3), 0 0 30px rgba(var(--v5-cyan-rgb), 0.08)",
|
||||||
fontFamily: "var(--v5-font-sans)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 브랜디드 헤더 */}
|
{/* 브랜디드 헤더 */}
|
||||||
@@ -123,8 +204,8 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.8rem",
|
gap: "0.95rem",
|
||||||
padding: "0.85rem 1.1rem",
|
padding: "1rem 1.3rem",
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
borderBottom: "1px solid var(--v5-border)",
|
borderBottom: "1px solid var(--v5-border)",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@@ -132,39 +213,37 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 30,
|
width: 38,
|
||||||
height: 30,
|
height: 38,
|
||||||
borderRadius: 9,
|
borderRadius: 10,
|
||||||
background: "linear-gradient(135deg, var(--v5-cyan), var(--v5-primary))",
|
background: "linear-gradient(135deg, var(--v5-cyan), var(--v5-primary))",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
boxShadow: "0 0 18px rgba(var(--v5-cyan-rgb), 0.4)",
|
boxShadow: "0 0 12px rgba(var(--v5-cyan-rgb), 0.3)",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Building2 size={16} strokeWidth={1.75} />
|
<Building2 size={20} strokeWidth={1.75} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.55rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: "0.18em",
|
letterSpacing: "0.02em",
|
||||||
color: "var(--v5-cyan)",
|
color: "var(--v5-cyan)",
|
||||||
textTransform: "uppercase",
|
|
||||||
fontFamily: "var(--v5-font-mono)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
INVION · PROVISIONING
|
회사 프로비저닝
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.92rem",
|
fontSize: "1.15rem",
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
letterSpacing: "-0.02em",
|
letterSpacing: "-0.02em",
|
||||||
marginTop: 1,
|
marginTop: 2,
|
||||||
color: "var(--v5-text)",
|
color: "var(--v5-text)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
@@ -182,22 +261,23 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.75rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
letterSpacing: "0.02em",
|
||||||
letterSpacing: "0.12em",
|
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
SUPER_ADMIN
|
최고관리자
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={tryClose}
|
onClick={tryClose}
|
||||||
aria-label="닫기"
|
aria-label="닫기"
|
||||||
|
className="wiz-iconbtn"
|
||||||
style={{
|
style={{
|
||||||
width: 28,
|
width: 34,
|
||||||
height: 28,
|
height: 34,
|
||||||
borderRadius: 7,
|
borderRadius: 8,
|
||||||
border: "1px solid var(--v5-border)",
|
border: "1px solid var(--v5-border)",
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
color: "var(--v5-text-sec)",
|
color: "var(--v5-text-sec)",
|
||||||
@@ -208,7 +288,7 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
{/* glow line */}
|
{/* glow line */}
|
||||||
<div
|
<div
|
||||||
@@ -246,12 +326,12 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
{/* Footer (상황별) */}
|
{/* Footer (상황별) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "0.75rem 1.1rem",
|
padding: "0.9rem 1.3rem",
|
||||||
borderTop: "1px solid var(--v5-border)",
|
borderTop: "1px solid var(--v5-border)",
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.5rem",
|
gap: "0.55rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{step < 4 && (
|
{step < 4 && (
|
||||||
@@ -259,10 +339,10 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
<Btn variant="ghost" onClick={tryClose}>
|
<Btn variant="ghost" onClick={tryClose}>
|
||||||
취소
|
취소
|
||||||
</Btn>
|
</Btn>
|
||||||
<div style={{ flex: 1, fontSize: "0.6rem", color: "var(--v5-text-muted)", textAlign: "center" }}>
|
<div style={{ flex: 1, fontSize: "0.75rem", color: "var(--v5-text-muted)", textAlign: "center", fontWeight: 500 }}>
|
||||||
{validByStep[step] ? "다음 단계로 이동 가능" : "필수 항목을 확인하세요"}
|
{validByStep[step] ? "다음 단계로 이동 가능" : "필수 항목을 확인하세요"}
|
||||||
</div>
|
</div>
|
||||||
<Btn variant="secondary" icon={<ArrowLeft size={11} strokeWidth={1.75} />} onClick={prev} disabled={step <= 1}>
|
<Btn variant="secondary" icon={<ArrowLeft size={13} strokeWidth={1.75} />} onClick={prev} disabled={step <= 1}>
|
||||||
이전
|
이전
|
||||||
</Btn>
|
</Btn>
|
||||||
{step < 3 ? (
|
{step < 3 ? (
|
||||||
@@ -270,13 +350,13 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
variant="cyan"
|
variant="cyan"
|
||||||
onClick={next}
|
onClick={next}
|
||||||
disabled={!canNext}
|
disabled={!canNext}
|
||||||
icon={<ArrowRight size={11} strokeWidth={1.75} />}
|
icon={<ArrowRight size={13} strokeWidth={1.75} />}
|
||||||
iconRight
|
iconRight
|
||||||
>
|
>
|
||||||
다음
|
다음
|
||||||
</Btn>
|
</Btn>
|
||||||
) : (
|
) : (
|
||||||
<Btn variant="cyan" onClick={next} disabled={!canNext} icon={<Rocket size={11} strokeWidth={1.75} />}>
|
<Btn variant="cyan" onClick={next} disabled={!canNext} icon={<Rocket size={13} strokeWidth={1.75} />}>
|
||||||
생성 시작
|
생성 시작
|
||||||
</Btn>
|
</Btn>
|
||||||
)}
|
)}
|
||||||
@@ -288,17 +368,18 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.76rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "rgb(var(--v5-green-rgb))",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
생성 완료 · AUDIT event 기록됨
|
생성 완료 · 감사 로그에 기록됨
|
||||||
</div>
|
</div>
|
||||||
<Btn icon={<FileText size={11} strokeWidth={1.75} />}>감사 로그</Btn>
|
<Btn icon={<FileText size={13} strokeWidth={1.75} />} disabled soon>감사 로그</Btn>
|
||||||
<Btn
|
<Btn
|
||||||
variant="cyan"
|
variant="cyan"
|
||||||
icon={<ArrowUpRight size={11} strokeWidth={1.75} />}
|
icon={<ArrowUpRight size={13} strokeWidth={1.75} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (runDone.subdomain) {
|
if (runDone.subdomain) {
|
||||||
window.open(`http://${runDone.subdomain}.invyone.com`, "_blank");
|
window.open(`http://${runDone.subdomain}.invyone.com`, "_blank");
|
||||||
@@ -306,7 +387,7 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
tryClose();
|
tryClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
바로 이동
|
회사 사이트 열기
|
||||||
</Btn>
|
</Btn>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -316,21 +397,21 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.76rem",
|
||||||
color: "var(--v5-red)",
|
color: "var(--v5-red)",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 5,
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AlertTriangle size={11} strokeWidth={1.75} />
|
<AlertTriangle size={14} strokeWidth={1.75} />
|
||||||
실패 · DB 가 미완성 상태일 수 있습니다
|
실패 · DB 가 미완성 상태일 수 있습니다
|
||||||
</div>
|
</div>
|
||||||
<Btn variant="danger" icon={<Trash2 size={11} strokeWidth={1.75} />}>
|
<Btn variant="danger" icon={<Trash2 size={13} strokeWidth={1.75} />} disabled soon>
|
||||||
롤백 (DB 삭제)
|
롤백 (DB 삭제)
|
||||||
</Btn>
|
</Btn>
|
||||||
<Btn variant="cyan" icon={<RefreshCw size={11} strokeWidth={1.75} />} onClick={tryClose}>
|
<Btn variant="cyan" icon={<RefreshCw size={13} strokeWidth={1.75} />} onClick={tryClose}>
|
||||||
닫기
|
닫기
|
||||||
</Btn>
|
</Btn>
|
||||||
</>
|
</>
|
||||||
@@ -341,17 +422,18 @@ export default function Wizard({ onClose }: { onClose: () => void }) {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.75rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 5,
|
gap: 6,
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Info size={11} strokeWidth={1.75} />
|
<Info size={13} strokeWidth={1.75} />
|
||||||
생성 중에는 닫아도 서버에서 계속 진행됩니다.
|
생성 중에는 닫아도 서버에서 계속 진행됩니다.
|
||||||
</div>
|
</div>
|
||||||
<Btn variant="ghost" icon={<XOctagon size={11} strokeWidth={1.75} />} onClick={tryClose}>
|
<Btn variant="ghost" icon={<XOctagon size={13} strokeWidth={1.75} />} onClick={tryClose}>
|
||||||
창 닫기
|
창 닫기
|
||||||
</Btn>
|
</Btn>
|
||||||
<Btn variant="secondary" disabled>
|
<Btn variant="secondary" disabled>
|
||||||
@@ -372,6 +454,7 @@ function Btn({
|
|||||||
variant = "secondary",
|
variant = "secondary",
|
||||||
icon,
|
icon,
|
||||||
iconRight,
|
iconRight,
|
||||||
|
soon,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@@ -379,6 +462,7 @@ function Btn({
|
|||||||
variant?: "primary" | "secondary" | "ghost" | "cyan" | "danger";
|
variant?: "primary" | "secondary" | "ghost" | "cyan" | "danger";
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
iconRight?: boolean;
|
iconRight?: boolean;
|
||||||
|
soon?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const styles: Record<string, React.CSSProperties> = {
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
primary: {
|
primary: {
|
||||||
@@ -390,7 +474,7 @@ function Btn({
|
|||||||
background: "var(--v5-cyan)",
|
background: "var(--v5-cyan)",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
borderColor: "var(--v5-cyan)",
|
borderColor: "var(--v5-cyan)",
|
||||||
boxShadow: disabled ? "none" : "0 0 18px rgba(var(--v5-cyan-rgb), 0.25)",
|
boxShadow: disabled ? "none" : "0 0 10px rgba(var(--v5-cyan-rgb), 0.15)",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
background: "var(--v5-surface-solid)",
|
background: "var(--v5-surface-solid)",
|
||||||
@@ -412,27 +496,48 @@ function Btn({
|
|||||||
<button
|
<button
|
||||||
onClick={disabled ? undefined : onClick}
|
onClick={disabled ? undefined : onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
title={soon ? "준비 중인 기능입니다" : undefined}
|
||||||
|
className={`wiz-btn wiz-btn-${variant}`}
|
||||||
style={{
|
style={{
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.35rem",
|
gap: "0.45rem",
|
||||||
height: 30,
|
height: 36,
|
||||||
padding: "0 0.75rem",
|
padding: "0 0.95rem",
|
||||||
borderRadius: 8,
|
borderRadius: 9,
|
||||||
fontSize: "0.7rem",
|
fontSize: "0.82rem",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
border: "1px solid transparent",
|
border: "1px solid transparent",
|
||||||
cursor: disabled ? "not-allowed" : "pointer",
|
cursor: disabled ? "not-allowed" : "pointer",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
transition: "all 0.2s cubic-bezier(0.4,0,0.2,1)",
|
opacity: disabled ? 0.5 : 1,
|
||||||
opacity: disabled ? 0.45 : 1,
|
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
...styles[variant],
|
...styles[variant],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!iconRight && icon}
|
{!iconRight && icon}
|
||||||
{children}
|
{children}
|
||||||
|
{soon && <SoonBadge />}
|
||||||
{iconRight && icon}
|
{iconRight && icon}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SoonBadge() {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
padding: "2px 6px",
|
||||||
|
borderRadius: 3,
|
||||||
|
background: "rgba(var(--v5-amber-rgb), 0.15)",
|
||||||
|
color: "var(--v5-amber)",
|
||||||
|
letterSpacing: "0.03em",
|
||||||
|
marginLeft: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
준비 중
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,23 +27,24 @@ export function Field({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div style={{ gridColumn: full ? "1 / -1" : "auto", display: "flex", flexDirection: "column", gap: 4 }}>
|
<div style={{ gridColumn: full ? "1 / -1" : "auto", display: "flex", flexDirection: "column", gap: 5 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.55rem",
|
fontSize: "0.7rem",
|
||||||
color: error ? "var(--v5-red)" : "var(--v5-text-muted)",
|
color: error ? "var(--v5-red)" : "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
letterSpacing: "0.08em",
|
letterSpacing: "0.08em",
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
marginBottom: 1,
|
marginBottom: 2,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 5,
|
gap: 6,
|
||||||
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{required && (
|
{required && (
|
||||||
<span style={{ color: "var(--v5-red)", fontFamily: "sans-serif" }}>•</span>
|
<span style={{ color: "var(--v5-red)", fontFamily: "sans-serif", fontSize: "0.9rem" }}>•</span>
|
||||||
)}
|
)}
|
||||||
{hint && (
|
{hint && (
|
||||||
<span
|
<span
|
||||||
@@ -53,7 +54,7 @@ export function Field({
|
|||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.72rem",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -63,7 +64,7 @@ export function Field({
|
|||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{ fontSize: "0.58rem", color: "var(--v5-red)", fontFamily: "var(--v5-font-mono)" }}>{error}</div>
|
<div style={{ fontSize: "0.72rem", color: "var(--v5-red)", fontFamily: "var(--v5-font-mono)" }}>{error}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -116,11 +117,11 @@ export function TextInput({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
border: `1px solid ${border}`,
|
border: `1px solid ${border}`,
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
background: readOnly ? "var(--v5-bg-subtle)" : "var(--v5-surface-solid)",
|
background: readOnly ? "var(--v5-bg-subtle)" : "var(--v5-surface-solid)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
boxShadow: glow,
|
boxShadow: glow,
|
||||||
transition: "all 0.15s",
|
transition: "all 0.18s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{prefix && (
|
{prefix && (
|
||||||
@@ -128,12 +129,13 @@ export function TextInput({
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "0 0.5rem",
|
padding: "0 0.65rem",
|
||||||
background: "var(--v5-bg-subtle)",
|
background: "var(--v5-bg-subtle)",
|
||||||
borderRight: "1px solid var(--v5-border)",
|
borderRight: "1px solid var(--v5-border)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.78rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{prefix}
|
{prefix}
|
||||||
@@ -147,11 +149,11 @@ export function TextInput({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: size === "sm" ? "0.3rem 0.45rem" : "0.42rem 0.55rem",
|
padding: size === "sm" ? "0.4rem 0.55rem" : "0.55rem 0.7rem",
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
border: 0,
|
border: 0,
|
||||||
outline: "none",
|
outline: "none",
|
||||||
fontSize: size === "sm" ? "0.65rem" : "0.7rem",
|
fontSize: size === "sm" ? "0.78rem" : "0.88rem",
|
||||||
color: "var(--v5-text)",
|
color: "var(--v5-text)",
|
||||||
fontFamily: mono ? "var(--v5-font-mono)" : "inherit",
|
fontFamily: mono ? "var(--v5-font-mono)" : "inherit",
|
||||||
}}
|
}}
|
||||||
@@ -161,12 +163,13 @@ export function TextInput({
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "0 0.5rem",
|
padding: "0 0.65rem",
|
||||||
background: "var(--v5-bg-subtle)",
|
background: "var(--v5-bg-subtle)",
|
||||||
borderLeft: "1px solid var(--v5-border)",
|
borderLeft: "1px solid var(--v5-border)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
fontSize: "0.62rem",
|
fontSize: "0.78rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{suffix}
|
{suffix}
|
||||||
@@ -178,7 +181,7 @@ export function TextInput({
|
|||||||
|
|
||||||
export function CheckAvailBadge({ status, value }: { status: AvailStatus; value?: string }) {
|
export function CheckAvailBadge({ status, value }: { status: AvailStatus; value?: string }) {
|
||||||
if (!value || status === "idle")
|
if (!value || status === "idle")
|
||||||
return <span style={{ fontSize: "0.58rem", color: "var(--v5-text-muted)" }}>입력 후 자동 확인</span>;
|
return <span style={{ fontSize: "0.72rem", color: "var(--v5-text-muted)" }}>입력 후 자동 확인</span>;
|
||||||
if (status === "checking")
|
if (status === "checking")
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -186,12 +189,13 @@ export function CheckAvailBadge({ status, value }: { status: AvailStatus; value?
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 5,
|
gap: 5,
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.72rem",
|
||||||
color: "var(--v5-text-muted)",
|
color: "var(--v5-text-muted)",
|
||||||
fontFamily: "var(--v5-font-mono)",
|
fontFamily: "var(--v5-font-mono)",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Loader2 size={10} className="spin" /> 중복 확인 중…
|
<Loader2 size={12} className="spin" /> 중복 확인 중…
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (status === "available")
|
if (status === "available")
|
||||||
@@ -201,12 +205,12 @@ export function CheckAvailBadge({ status, value }: { status: AvailStatus; value?
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.72rem",
|
||||||
color: "rgb(var(--v5-green-rgb))",
|
color: "rgb(var(--v5-green-rgb))",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CheckCircle2 size={11} /> 사용 가능
|
<CheckCircle2 size={13} /> 사용 가능
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (status === "taken")
|
if (status === "taken")
|
||||||
@@ -216,12 +220,12 @@ export function CheckAvailBadge({ status, value }: { status: AvailStatus; value?
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.72rem",
|
||||||
color: "var(--v5-red)",
|
color: "var(--v5-red)",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XCircle size={11} /> 이미 사용 중
|
<XCircle size={13} /> 이미 사용 중
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@@ -230,12 +234,12 @@ export function CheckAvailBadge({ status, value }: { status: AvailStatus; value?
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
fontSize: "0.58rem",
|
fontSize: "0.72rem",
|
||||||
color: "var(--v5-amber)",
|
color: "var(--v5-amber)",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XCircle size={11} /> 형식 오류
|
<XCircle size={13} /> 형식 오류
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ const getApiBaseUrl = (): string => {
|
|||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const currentHost = window.location.hostname;
|
const currentHost = window.location.hostname;
|
||||||
|
|
||||||
// 1. 테넌트 서브도메인 (*.invyone.com) 은 Next.js rewrite 우회 필수.
|
// 1. 테넌트 서브도메인 (*.invyone.com) 은 NEXT_PUBLIC_API_URL rewrite 우회 필수.
|
||||||
// NEXT_PUBLIC_API_URL=/api 쓰면 rewrite 가 Host 헤더를 invyone-backend-spring:8081 로
|
// rewrite 가 Host 헤더를 변조하면 SubdomainResolverFilter 파싱 실패.
|
||||||
// 변조해서 서브도메인 파싱이 실패함. 직접 8083 으로 보내서 Host 헤더 보존.
|
// 운영 Traefik 이 같은 호스트의 /api 를 backend 로 프록시 → 동일 호스트 상대 주소.
|
||||||
if (currentHost.endsWith(".invyone.com")) {
|
if (currentHost.endsWith(".invyone.com")) {
|
||||||
return `http://${currentHost}:8083/api`;
|
return `https://${currentHost}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 프로덕션 메인 도메인
|
// 2. 프로덕션 메인 도메인
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ spec:
|
|||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: invyone-config
|
name: invyone-config
|
||||||
key: FILE_UPLOAD_DIR
|
key: FILE_UPLOAD_DIR
|
||||||
|
- name: CORS_ALLOWED_ORIGINS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: invyone-config
|
||||||
|
key: CORS_ALLOWED_ORIGINS
|
||||||
|
- name: TENANT_PROVISIONING_REQUIRE_SUPER_ADMIN
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: invyone-config
|
||||||
|
key: TENANT_PROVISIONING_REQUIRE_SUPER_ADMIN
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 250m
|
cpu: 250m
|
||||||
|
|||||||
@@ -16,3 +16,7 @@ data:
|
|||||||
CORS_ORIGIN: "https://solution.invyone.com"
|
CORS_ORIGIN: "https://solution.invyone.com"
|
||||||
NEXT_PUBLIC_API_URL: "https://solution.invyone.com/api"
|
NEXT_PUBLIC_API_URL: "https://solution.invyone.com/api"
|
||||||
NEXT_TELEMETRY_DISABLED: "1"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
|
# 테넌트 서브도메인 멀티테넌시: 허용 CORS 오리진 (setAllowedOriginPatterns 매칭)
|
||||||
|
CORS_ALLOWED_ORIGINS: "https://solution.invyone.com,https://*.invyone.com,http://*.invyone.com:[*],https://*.invyone.com:[*]"
|
||||||
|
# 프로덕션: SUPER_ADMIN 만 회사 프로비저닝 허용 (개발 모드는 false)
|
||||||
|
TENANT_PROVISIONING_REQUIRE_SUPER_ADMIN: "true"
|
||||||
|
|||||||
Reference in New Issue
Block a user