feat: Enhance Git tools installation process with progress tracking and Windows path management. Added UI updates for installation steps, including status and progress indicators, to improve user experience during setup.
This commit is contained in:
parent
1573df675b
commit
c78159d442
|
|
@ -0,0 +1 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "yarn build:main && concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_OPTIONS= VITE_DEV_SERVER_URL=http://localhost:5173 VITE_DEV_UI=1 electron .\"",
|
"dev": "yarn build:main && concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_OPTIONS= VITE_DEV_SERVER_URL=http://localhost:5173 VITE_DEV_UI=1 electron .\"",
|
||||||
|
"dev:ui": "yarn build:main && concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_OPTIONS= VITE_DEV_SERVER_URL=http://localhost:5173 VITE_DEV_UI=1 electron .\"",
|
||||||
"build:renderer": "vite build",
|
"build:renderer": "vite build",
|
||||||
"build:main": "tsc -p tsconfig.main.json",
|
"build:main": "tsc -p tsconfig.main.json",
|
||||||
"build": "yarn build:renderer && yarn build:main",
|
"build": "yarn build:renderer && yarn build:main",
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ type InstallStep = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
result?: CommandRunResult;
|
result?: CommandRunResult;
|
||||||
|
status?: "running" | "done" | "error";
|
||||||
|
progress?: { current: number; total: number; percent: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
type InstallGitToolsResult = {
|
type InstallGitToolsResult = {
|
||||||
|
|
@ -194,7 +196,35 @@ const checkCommand = async (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshWindowsPath = async () => {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await runCommand(
|
||||||
|
"powershell",
|
||||||
|
[
|
||||||
|
"-NoProfile",
|
||||||
|
"-Command",
|
||||||
|
"[Environment]::GetEnvironmentVariable('Path','Machine');[Environment]::GetEnvironmentVariable('Path','User')",
|
||||||
|
],
|
||||||
|
COMMAND_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
if (!result.ok || !result.output) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const merged = result.output
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(";");
|
||||||
|
const combined = [merged, process.env.PATH].filter(Boolean).join(";");
|
||||||
|
const unique = Array.from(new Set(combined.split(";").filter(Boolean))).join(";");
|
||||||
|
process.env.PATH = unique;
|
||||||
|
process.env.Path = unique;
|
||||||
|
};
|
||||||
|
|
||||||
const checkGitTools = async () => {
|
const checkGitTools = async () => {
|
||||||
|
await refreshWindowsPath();
|
||||||
const git = await checkCommand("git", ["--version"]);
|
const git = await checkCommand("git", ["--version"]);
|
||||||
const lfs = await checkCommand("git", ["lfs", "version"]);
|
const lfs = await checkCommand("git", ["lfs", "version"]);
|
||||||
|
|
||||||
|
|
@ -306,6 +336,7 @@ const runCommand = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGitPaths = async () => {
|
const getGitPaths = async () => {
|
||||||
|
await refreshWindowsPath();
|
||||||
const git = await resolveCommandPath("git");
|
const git = await resolveCommandPath("git");
|
||||||
const lfs = await resolveCommandPath("git-lfs");
|
const lfs = await resolveCommandPath("git-lfs");
|
||||||
|
|
||||||
|
|
@ -985,6 +1016,24 @@ const installGitTools = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps: InstallStep[] = [];
|
const steps: InstallStep[] = [];
|
||||||
|
const totalSteps = 3;
|
||||||
|
const emitProgress = (
|
||||||
|
name: string,
|
||||||
|
current: number,
|
||||||
|
status: InstallStep["status"],
|
||||||
|
message?: string,
|
||||||
|
result?: CommandRunResult,
|
||||||
|
) => {
|
||||||
|
const percent = Math.round((current / totalSteps) * 100);
|
||||||
|
onProgress?.({
|
||||||
|
name,
|
||||||
|
ok: status !== "error",
|
||||||
|
message,
|
||||||
|
status,
|
||||||
|
progress: { current, total: totalSteps, percent },
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const wingetPath = await resolveCommandPath("winget");
|
const wingetPath = await resolveCommandPath("winget");
|
||||||
if (!wingetPath.ok) {
|
if (!wingetPath.ok) {
|
||||||
|
|
@ -998,6 +1047,8 @@ const installGitTools = async (
|
||||||
args: ["--version"],
|
args: ["--version"],
|
||||||
error: wingetPath.error,
|
error: wingetPath.error,
|
||||||
},
|
},
|
||||||
|
status: "error" as const,
|
||||||
|
progress: { current: 1, total: totalSteps, percent: 33 },
|
||||||
};
|
};
|
||||||
steps.push(errorStep);
|
steps.push(errorStep);
|
||||||
onProgress?.(errorStep);
|
onProgress?.(errorStep);
|
||||||
|
|
@ -1020,14 +1071,18 @@ const installGitTools = async (
|
||||||
// skipping separate success step for winget to match previous behavior purely?
|
// skipping separate success step for winget to match previous behavior purely?
|
||||||
// User asked for "same template", so seeing "Winget Check... OK" is good.
|
// User asked for "same template", so seeing "Winget Check... OK" is good.
|
||||||
// I will add it for clarity.
|
// I will add it for clarity.
|
||||||
|
emitProgress("winget 확인", 1, "running");
|
||||||
const wingetStep = {
|
const wingetStep = {
|
||||||
name: "winget 확인",
|
name: "winget 확인",
|
||||||
ok: true,
|
ok: true,
|
||||||
result: { ok: true, command: "winget", args: ["--version"] }
|
result: { ok: true, command: "winget", args: ["--version"] },
|
||||||
|
status: "done" as const,
|
||||||
|
progress: { current: 1, total: totalSteps, percent: 33 },
|
||||||
};
|
};
|
||||||
steps.push(wingetStep);
|
steps.push(wingetStep);
|
||||||
onProgress?.(wingetStep);
|
onProgress?.(wingetStep);
|
||||||
|
|
||||||
|
emitProgress("Git 설치", 2, "running");
|
||||||
const gitInstall = await runCommand(
|
const gitInstall = await runCommand(
|
||||||
"winget",
|
"winget",
|
||||||
[
|
[
|
||||||
|
|
@ -1044,18 +1099,18 @@ const installGitTools = async (
|
||||||
],
|
],
|
||||||
INSTALL_TIMEOUT_MS,
|
INSTALL_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
steps.push({
|
const gitInstallStep: InstallStep = {
|
||||||
name: "Git 설치",
|
name: "Git 설치",
|
||||||
ok: gitInstall.ok,
|
ok: gitInstall.ok,
|
||||||
message: gitInstall.output || gitInstall.error, // Prioritize output
|
message: gitInstall.ok ? undefined : (gitInstall.error ?? gitInstall.output),
|
||||||
result: gitInstall
|
result: gitInstall,
|
||||||
});
|
status: gitInstall.ok ? "done" : "error",
|
||||||
onProgress?.({
|
progress: { current: 2, total: totalSteps, percent: 67 },
|
||||||
name: "Git 설치",
|
};
|
||||||
ok: gitInstall.ok,
|
steps.push(gitInstallStep);
|
||||||
message: gitInstall.output || gitInstall.error // Prioritize output
|
onProgress?.(gitInstallStep);
|
||||||
});
|
|
||||||
|
|
||||||
|
emitProgress("Git LFS 설치", 3, "running");
|
||||||
const lfsInstall = await runCommand(
|
const lfsInstall = await runCommand(
|
||||||
"winget",
|
"winget",
|
||||||
[
|
[
|
||||||
|
|
@ -1072,19 +1127,19 @@ const installGitTools = async (
|
||||||
],
|
],
|
||||||
INSTALL_TIMEOUT_MS,
|
INSTALL_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
steps.push({
|
const lfsInstallStep: InstallStep = {
|
||||||
name: "Git LFS 설치",
|
name: "Git LFS 설치",
|
||||||
ok: lfsInstall.ok,
|
ok: lfsInstall.ok,
|
||||||
message: lfsInstall.output || lfsInstall.error, // Prioritize output
|
message: lfsInstall.ok ? undefined : (lfsInstall.error ?? lfsInstall.output),
|
||||||
result: lfsInstall
|
result: lfsInstall,
|
||||||
});
|
status: lfsInstall.ok ? "done" : "error",
|
||||||
onProgress?.({
|
progress: { current: 3, total: totalSteps, percent: 100 },
|
||||||
name: "Git LFS 설치",
|
};
|
||||||
ok: lfsInstall.ok,
|
steps.push(lfsInstallStep);
|
||||||
message: lfsInstall.output || lfsInstall.error // Prioritize output
|
onProgress?.(lfsInstallStep);
|
||||||
});
|
|
||||||
|
|
||||||
const ok = steps.every((step) => step.ok);
|
const ok = steps.every((step) => step.ok);
|
||||||
|
await refreshWindowsPath();
|
||||||
const check = await checkGitTools();
|
const check = await checkGitTools();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ const App = () => {
|
||||||
const [skipToolsCheck, setSkipToolsCheck] = useState(false);
|
const [skipToolsCheck, setSkipToolsCheck] = useState(false);
|
||||||
const [devProceedReady, setDevProceedReady] = useState(false);
|
const [devProceedReady, setDevProceedReady] = useState(false);
|
||||||
const [simulateToolsMissing, setSimulateToolsMissing] = useState(false);
|
const [simulateToolsMissing, setSimulateToolsMissing] = useState(false);
|
||||||
|
const [simulateInstallProgress, setSimulateInstallProgress] = useState(false);
|
||||||
const [gitCheckInProgress, setGitCheckInProgress] = useState(false);
|
const [gitCheckInProgress, setGitCheckInProgress] = useState(false);
|
||||||
const [gitCheckedAt, setGitCheckedAt] = useState<number | undefined>();
|
const [gitCheckedAt, setGitCheckedAt] = useState<number | undefined>();
|
||||||
const [gitCheckResult, setGitCheckResult] = useState<GitToolsCheck | undefined>();
|
const [gitCheckResult, setGitCheckResult] = useState<GitToolsCheck | undefined>();
|
||||||
|
|
@ -100,8 +101,17 @@ const App = () => {
|
||||||
const sessionResumeAttemptedRef = useRef(false);
|
const sessionResumeAttemptedRef = useRef(false);
|
||||||
const transitionTimeoutRef = useRef<number | undefined>();
|
const transitionTimeoutRef = useRef<number | undefined>();
|
||||||
const simulateToolsMissingRef = useRef(false);
|
const simulateToolsMissingRef = useRef(false);
|
||||||
|
const devInstallTimerRef = useRef<number | undefined>();
|
||||||
const [modalTitle, setModalTitle] = useState("모드 적용 중...");
|
const [modalTitle, setModalTitle] = useState("모드 적용 중...");
|
||||||
const [syncSteps, setSyncSteps] = useState<Array<{ name: string; ok: boolean; message?: string }>>([]);
|
const [syncSteps, setSyncSteps] = useState<
|
||||||
|
Array<{
|
||||||
|
name: string;
|
||||||
|
ok: boolean;
|
||||||
|
message?: string;
|
||||||
|
status?: "running" | "done" | "error";
|
||||||
|
progress?: { current: number; total: number; percent: number };
|
||||||
|
}>
|
||||||
|
>([]);
|
||||||
const [syncResult, setSyncResult] = useState<{ ok: boolean; error?: string } | undefined>();
|
const [syncResult, setSyncResult] = useState<{ ok: boolean; error?: string } | undefined>();
|
||||||
|
|
||||||
const getStepDisplayInfo = (rawName: string) => {
|
const getStepDisplayInfo = (rawName: string) => {
|
||||||
|
|
@ -134,6 +144,27 @@ const App = () => {
|
||||||
return { title: rawName, command: "" };
|
return { title: rawName, command: "" };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const upsertSyncStep = useCallback(
|
||||||
|
(nextStep: {
|
||||||
|
name: string;
|
||||||
|
ok: boolean;
|
||||||
|
message?: string;
|
||||||
|
status?: "running" | "done" | "error";
|
||||||
|
progress?: { current: number; total: number; percent: number };
|
||||||
|
}) => {
|
||||||
|
setSyncSteps((prev) => {
|
||||||
|
const index = prev.findIndex((step) => step.name === nextStep.name);
|
||||||
|
if (index < 0) {
|
||||||
|
return [...prev, nextStep];
|
||||||
|
}
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[index] = { ...updated[index], ...nextStep };
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const asObject = (value: unknown) =>
|
const asObject = (value: unknown) =>
|
||||||
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||||
const pickString = (...values: unknown[]) =>
|
const pickString = (...values: unknown[]) =>
|
||||||
|
|
@ -512,6 +543,14 @@ const App = () => {
|
||||||
simulateToolsMissingRef.current = simulateToolsMissing;
|
simulateToolsMissingRef.current = simulateToolsMissing;
|
||||||
}, [simulateToolsMissing]);
|
}, [simulateToolsMissing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (devInstallTimerRef.current) {
|
||||||
|
window.clearTimeout(devInstallTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => clearTransitionTimeout();
|
return () => clearTransitionTimeout();
|
||||||
}, [clearTransitionTimeout]);
|
}, [clearTransitionTimeout]);
|
||||||
|
|
@ -578,6 +617,15 @@ const App = () => {
|
||||||
return Array.from(tags);
|
return Array.from(tags);
|
||||||
}, [modVersionStatus]);
|
}, [modVersionStatus]);
|
||||||
|
|
||||||
|
const syncProgress = useMemo(() => {
|
||||||
|
for (let i = syncSteps.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (syncSteps[i].progress) {
|
||||||
|
return { progress: syncSteps[i].progress, stepName: syncSteps[i].name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [syncSteps]);
|
||||||
|
|
||||||
const startReady = Boolean(sptInstallInfo?.ok && modVersionStatus?.ok && !syncInProgress);
|
const startReady = Boolean(sptInstallInfo?.ok && modVersionStatus?.ok && !syncInProgress);
|
||||||
const startStatusLabel = syncInProgress
|
const startStatusLabel = syncInProgress
|
||||||
? "동기화 중"
|
? "동기화 중"
|
||||||
|
|
@ -655,7 +703,14 @@ const App = () => {
|
||||||
setSyncInProgress(true);
|
setSyncInProgress(true);
|
||||||
|
|
||||||
const cleanup = window.sptLauncher.onGitInstallProgress((_e, step: any) => {
|
const cleanup = window.sptLauncher.onGitInstallProgress((_e, step: any) => {
|
||||||
setSyncSteps((prev) => [...prev, step]);
|
const normalized = {
|
||||||
|
name: step.name,
|
||||||
|
ok: Boolean(step.ok),
|
||||||
|
message: step.ok ? undefined : step.message,
|
||||||
|
status: step.status,
|
||||||
|
progress: step.progress
|
||||||
|
};
|
||||||
|
upsertSyncStep(normalized);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -682,6 +737,60 @@ const App = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runDevInstallSimulation = useCallback(() => {
|
||||||
|
if (!DEV_UI_ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (devInstallTimerRef.current) {
|
||||||
|
window.clearTimeout(devInstallTimerRef.current);
|
||||||
|
}
|
||||||
|
setSimulateInstallProgress(true);
|
||||||
|
setInstallInProgress(true);
|
||||||
|
setInstallMessage(undefined);
|
||||||
|
setSyncSteps([]);
|
||||||
|
setSyncResult(undefined);
|
||||||
|
setModalTitle("Git 도구 설치 중...");
|
||||||
|
setSyncInProgress(true);
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ name: "winget 확인", percent: 33 },
|
||||||
|
{ name: "Git 설치", percent: 67 },
|
||||||
|
{ name: "Git LFS 설치", percent: 100 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
const tick = () => {
|
||||||
|
const current = steps[index];
|
||||||
|
upsertSyncStep({
|
||||||
|
name: current.name,
|
||||||
|
ok: true,
|
||||||
|
status: "running",
|
||||||
|
progress: { current: index + 1, total: steps.length, percent: current.percent }
|
||||||
|
});
|
||||||
|
|
||||||
|
devInstallTimerRef.current = window.setTimeout(() => {
|
||||||
|
upsertSyncStep({
|
||||||
|
name: current.name,
|
||||||
|
ok: true,
|
||||||
|
status: "done",
|
||||||
|
progress: { current: index + 1, total: steps.length, percent: current.percent }
|
||||||
|
});
|
||||||
|
index += 1;
|
||||||
|
if (index < steps.length) {
|
||||||
|
tick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInstallMessage("자동 설치가 완료되었습니다.");
|
||||||
|
setSyncResult({ ok: true });
|
||||||
|
setInstallInProgress(false);
|
||||||
|
setSyncInProgress(false);
|
||||||
|
setSimulateInstallProgress(false);
|
||||||
|
}, 800);
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
}, [upsertSyncStep]);
|
||||||
|
|
||||||
const handleFetchGitPaths = async () => {
|
const handleFetchGitPaths = async () => {
|
||||||
if (!window.sptLauncher?.getGitPaths) {
|
if (!window.sptLauncher?.getGitPaths) {
|
||||||
window.alert("경로 확인 기능이 준비되지 않았습니다.");
|
window.alert("경로 확인 기능이 준비되지 않았습니다.");
|
||||||
|
|
@ -930,19 +1039,13 @@ const App = () => {
|
||||||
? window.sptLauncher.onModSyncProgress((_event, step) => {
|
? window.sptLauncher.onModSyncProgress((_event, step) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const s = step as any;
|
const s = step as any;
|
||||||
setSyncSteps((prev) => {
|
|
||||||
// Update existing step if name matches, or add new
|
|
||||||
// Ideally steps come in order. Let's just append or update last?
|
|
||||||
// The event sends "completed" steps usually?
|
|
||||||
// My main implementation sends: { name, result: { ok... } }
|
|
||||||
// I'll map it to simplified UI step
|
|
||||||
const uiStep = {
|
const uiStep = {
|
||||||
name: s.name,
|
name: s.name,
|
||||||
ok: s.result?.ok ?? false,
|
ok: s.result?.ok ?? false,
|
||||||
message: s.result?.message ?? s.result?.error
|
message: s.result?.ok ? undefined : s.result?.message ?? s.result?.error,
|
||||||
|
status: s.result?.ok ? "done" : "error"
|
||||||
};
|
};
|
||||||
return [...prev, uiStep];
|
upsertSyncStep(uiStep);
|
||||||
});
|
|
||||||
})
|
})
|
||||||
: () => {};
|
: () => {};
|
||||||
|
|
||||||
|
|
@ -959,11 +1062,14 @@ const App = () => {
|
||||||
// If steps are returned in result (history), use them?
|
// If steps are returned in result (history), use them?
|
||||||
// The real-time listener should have captured them, but let's ensure we have full list
|
// The real-time listener should have captured them, but let's ensure we have full list
|
||||||
if (result.steps) {
|
if (result.steps) {
|
||||||
setSyncSteps(result.steps.map(s => ({
|
setSyncSteps(
|
||||||
|
result.steps.map((s) => ({
|
||||||
name: s.name,
|
name: s.name,
|
||||||
ok: s.ok,
|
ok: s.ok,
|
||||||
message: s.message
|
message: s.ok ? undefined : s.message,
|
||||||
})));
|
status: s.ok ? "done" : "error"
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setModCheckMessage(result.error ?? "모드 동기화에 실패했습니다.");
|
setModCheckMessage(result.error ?? "모드 동기화에 실패했습니다.");
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -972,11 +1078,14 @@ const App = () => {
|
||||||
setSyncResult({ ok: true });
|
setSyncResult({ ok: true });
|
||||||
// Ensure steps from result are shown (more reliable final state)
|
// Ensure steps from result are shown (more reliable final state)
|
||||||
if (result.steps) {
|
if (result.steps) {
|
||||||
setSyncSteps(result.steps.map(s => ({
|
setSyncSteps(
|
||||||
|
result.steps.map((s) => ({
|
||||||
name: s.name,
|
name: s.name,
|
||||||
ok: s.ok,
|
ok: s.ok,
|
||||||
message: s.message
|
message: s.ok ? undefined : s.message,
|
||||||
})));
|
status: s.ok ? "done" : "error"
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyModSyncResult(result.tag ?? targetTag);
|
await applyModSyncResult(result.tag ?? targetTag);
|
||||||
|
|
@ -1142,6 +1251,16 @@ const App = () => {
|
||||||
{simulateToolsMissing ? "Git/LFS 시뮬레이션 해제" : "Git/LFS 미설치 시뮬레이션"}
|
{simulateToolsMissing ? "Git/LFS 시뮬레이션 해제" : "Git/LFS 미설치 시뮬레이션"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{DEV_UI_ENABLED && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost"
|
||||||
|
onClick={runDevInstallSimulation}
|
||||||
|
disabled={simulateInstallProgress || syncInProgress || installInProgress}
|
||||||
|
>
|
||||||
|
{simulateInstallProgress ? "설치 진행 시뮬레이션 중..." : "설치 진행 시뮬레이션"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1478,18 +1597,43 @@ const App = () => {
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
|
{syncProgress?.progress && (
|
||||||
|
<div className="sync-progress">
|
||||||
|
<div className="sync-progress-header">
|
||||||
|
<span>진행률(Progress)</span>
|
||||||
|
<span>{syncProgress.progress.percent}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="sync-progress-bar">
|
||||||
|
<div
|
||||||
|
className="sync-progress-fill"
|
||||||
|
style={{ width: `${syncProgress.progress.percent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{syncProgress.stepName && (
|
||||||
|
<div className="sync-progress-step">
|
||||||
|
현재 단계(Current Step): {getStepDisplayInfo(syncProgress.stepName).title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="sync-steps">
|
<div className="sync-steps">
|
||||||
{syncSteps.map((step, idx) => {
|
{syncSteps.map((step, idx) => {
|
||||||
const info = getStepDisplayInfo(step.name);
|
const info = getStepDisplayInfo(step.name);
|
||||||
|
const stepClass =
|
||||||
|
step.status === "running" ? "running" : step.ok ? "ok" : "fail";
|
||||||
|
const stepIcon =
|
||||||
|
step.status === "running" ? "⏳" : step.ok ? "✅" : "❌";
|
||||||
return (
|
return (
|
||||||
<div key={idx} className={`sync-step ${step.ok ? "ok" : "fail"}`}>
|
<div key={idx} className={`sync-step ${stepClass}`}>
|
||||||
<span className="step-number">{idx + 1}.</span>
|
<span className="step-number">{idx + 1}.</span>
|
||||||
<div className="step-content">
|
<div className="step-content">
|
||||||
<div className="step-title">{info.title}</div>
|
<div className="step-title">{info.title}</div>
|
||||||
<div className="step-command">{info.command}</div>
|
<div className="step-command">{info.command}</div>
|
||||||
{step.message && <div className="step-msg">Error: {step.message}</div>}
|
{!step.ok && step.message && (
|
||||||
|
<div className="step-msg">오류(Error): {step.message}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="step-icon">{step.ok ? "✅" : "❌"}</span>
|
<span className="step-icon">{stepIcon}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -710,6 +710,11 @@ button.ghost:hover:not(:disabled) {
|
||||||
background: rgba(16, 185, 129, 0.05);
|
background: rgba(16, 185, 129, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sync-step.running {
|
||||||
|
border-left: 3px solid var(--status-warn);
|
||||||
|
background: rgba(245, 158, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
.sync-step.fail {
|
.sync-step.fail {
|
||||||
border-left: 3px solid var(--status-error);
|
border-left: 3px solid var(--status-error);
|
||||||
background: rgba(239, 68, 68, 0.05);
|
background: rgba(239, 68, 68, 0.05);
|
||||||
|
|
@ -769,6 +774,42 @@ button.ghost:hover:not(:disabled) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sync-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--primary-hover));
|
||||||
|
transition: width 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-progress-step {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue