From debb38a29bb0377230bb6f852d4b285706a9c650 Mon Sep 17 00:00:00 2001 From: chyoo Date: Tue, 13 Oct 2026 15:08:12 +0900 Subject: [PATCH] fix: detect remote SLAM bridge and clear loading state on response --- package.json | 2 +- src/main/bridgeArtifacts.ts | 6 +- src/main/index.ts | 25 ++++- src/renderer/public/slam-viewer/index.html | 4 +- src/renderer/src/App.tsx | 23 ++-- .../src/components/AppStatusModals.tsx | 2 +- src/renderer/src/components/TitleBar.tsx | 2 +- .../curriculum/CurriculumHomeScreen.tsx | 2 +- .../features/curriculum/LearningInfoCard.tsx | 4 +- .../src/features/platforms/PlatformCards.tsx | 8 +- .../features/workspace/ModuleListHeader.tsx | 2 +- .../src/features/workspace/ModuleListRow.tsx | 8 +- .../features/workspace/ModuleListToolbar.tsx | 6 +- .../src/features/workspace/ModuleListView.tsx | 2 +- .../workspace/WorkspaceEditorPane.tsx | 2 +- .../workspace/WorkspaceEditorTabs.tsx | 2 +- .../features/workspace/WorkspaceFilePanel.tsx | 101 ++++++++++++++++-- .../workspace/WorkspaceGuidePanel.tsx | 8 +- .../features/workspace/WorkspaceLogView.tsx | 2 +- .../workspace/WorkspaceModuleTabs.tsx | 2 +- .../src/features/workspace/WorkspaceNav.tsx | 2 +- .../features/workspace/WorkspaceQaPanel.tsx | 4 +- .../workspace/WorkspaceRosConnectionsView.tsx | 6 +- .../workspace/WorkspaceSourceControlView.tsx | 6 +- .../features/workspace/WorldEditorModal.tsx | 73 ++++++------- .../workspace/useWorkspaceFileActions.ts | 55 ++++++++++ src/renderer/src/i18n/en.json | 11 +- 27 files changed, 270 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 57d642f..daa3e71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lms", - "version": "1.2.6", + "version": "1.2.7", "description": "Autonomy Studio LMS Electron app", "main": "./out/main/index.js", "author": { diff --git a/src/main/bridgeArtifacts.ts b/src/main/bridgeArtifacts.ts index 69bd417..9b34139 100644 --- a/src/main/bridgeArtifacts.ts +++ b/src/main/bridgeArtifacts.ts @@ -15,9 +15,9 @@ const artifacts = [ { image: jetsonBridgeImage, architecture: 'arm64', - url: `${root}/arm64/20260819-0659d4cf5578/slam-ros-bridge-jetson-orin.tar.zst`, - fileName: 'slam-ros-bridge-jetson-orin-0659d4cf5578.tar.zst', - checksumSha256: '0659d4cf5578c43bc9c25e4a4fc5d226ccd5b7bec43becfdff83b5390717114e', + url: `${root}/arm64/20261013-21acdf0cbdee/slam-ros-bridge-jetson-orin.tar.zst`, + fileName: 'slam-ros-bridge-jetson-orin-21acdf0cbdee.tar.zst', + checksumSha256: '21acdf0cbdee45d8b7b9dba78c3d321d67d8d7a2467511c4af0826e36ea0c74f', required: true, }, ] diff --git a/src/main/index.ts b/src/main/index.ts index 22c0086..e7536cd 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2462,9 +2462,12 @@ async function readRemoteVisualizationBridgeStatus(target: RemoteWorkspaceTarget 'docker ps --format "{{.Names}}\\t{{.Status}}" 2>/dev/null || true', 10_000, ) - const nav2 = output.split('\n').find((line) => line.startsWith('nav2-ros-bridge\t')) - const autoware = output.split('\n').find((line) => line.startsWith('autoware-ros-bridge\t')) + const lines = output.split('\n') + const nav2 = lines.find((line) => line.startsWith('nav2-ros-bridge\t')) + const slam = lines.find((line) => line.startsWith('slam-ros-bridge\t')) + const autoware = lines.find((line) => line.startsWith('autoware-ros-bridge\t')) if (nav2) return { ok: true, running: true, stack: 'nav2' } + if (slam) return { ok: true, running: true, stack: 'slam' } if (autoware) return { ok: true, running: true, stack: 'autoware' } return { ok: true, running: false, stack: null } } catch (error) { @@ -2472,6 +2475,18 @@ async function readRemoteVisualizationBridgeStatus(target: RemoteWorkspaceTarget } } +const managedVisualizationBridgeEnvKeys = new Set([ + 'ROS_DOMAIN_ID', + 'ROS_LOCALHOST_ONLY', + 'FASTDDS_DEFAULT_PROFILES_FILE', + 'FASTRTPS_DEFAULT_PROFILES_FILE', + 'AUTONOMY_STUDIO_ROS_NETWORK', +]) + +function isManagedVisualizationBridgeEnvKey(key: string): boolean { + return managedVisualizationBridgeEnvKeys.has(key) +} + async function startRemoteVisualizationBridge( target: RemoteWorkspaceTarget, stackInput?: string | null, @@ -2486,7 +2501,7 @@ async function startRemoteVisualizationBridge( const fastDdsConfigPath = await prepareRemoteRosNetworkConfig(normalized, '~/.autonomy-studio/runtime/nav2', rosNetwork, rosDomainId) const containerName = visualizationBridgeContainerName(stack) const bridgeEnvArgs = Object.entries(environmentProfile?.bridge?.env ?? {}) - .filter(([key]) => /^[A-Z_][A-Z0-9_]*$/.test(key)) + .filter(([key]) => /^[A-Z_][A-Z0-9_]*$/.test(key) && !isManagedVisualizationBridgeEnvKey(key)) .map(([key, value]) => `-e ${quoteShellSingle(`${key}=${value}`)}`) .join(' ') let image = resolveVisualizationBridgeImage(stack, environmentProfile?.bridge, environmentProfile?.runtimeImages) @@ -4022,7 +4037,9 @@ async function startVisualizationBridge( CMD_VEL_TOPIC: '/cmd_vel', NAVIGATE_TO_POSE_ACTION: '/navigate_to_pose' } : {}) - Object.entries(bridgeEnv).forEach(([key, value]) => args.push('-e', `${key}=${value}`)) + Object.entries(bridgeEnv).forEach(([key, value]) => { + if (!isManagedVisualizationBridgeEnvKey(key)) args.push('-e', `${key}=${value}`) + }) if (usesSlamBridge) { args.push('-e', `PORT=${bridgePort}`) } diff --git a/src/renderer/public/slam-viewer/index.html b/src/renderer/public/slam-viewer/index.html index b0c9d81..f434c5c 100644 --- a/src/renderer/public/slam-viewer/index.html +++ b/src/renderer/public/slam-viewer/index.html @@ -3135,6 +3135,9 @@ renderer.domElement.addEventListener('click', async (event) => { }); function applySlamState(state) { + // Any valid state payload confirms that the bridge is responding. Missing + // ROS data is reported separately by the topic status indicators. + markVisualizationResponseReceived(); latestSlamState = state; latestSlamStateReceivedAt = Date.now(); resetNavigationPoseForNewSession(state); @@ -3150,7 +3153,6 @@ function applySlamState(state) { clearNavigationGoalMarker(); navigationGoalId = null; } - if (state?.pose || state?.pose_3d || state?.scan || state?.map || state?.point_cloud || state?.sensor_point_cloud) markVisualizationResponseReceived(); if (NAV2_VIEW_MODE === 'navigation' && state?.tf?.map_to_odom) { navigationMapToOdom = state.tf.map_to_odom; updateNavigationMapTransform(); diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ad4b68d..d0ec402 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -990,7 +990,7 @@ function LocalPcWorkspaceMockup({ const surface = dark ? 'bg-[#202020] text-[#e6e6e6]' : 'bg-[#eef1f6] text-[#151827]' const topbar = dark ? 'border-white/10 bg-[#303030]' - : 'border-[#cbbdde] bg-[linear-gradient(90deg,#e6ddf1_0%,#e5def1_52%,#dce9f5_100%)]' + : 'border-[#cbbdde] bg-[linear-gradient(90deg,#f8f6fb_0%,#f5f2f9_52%,#dce9f5_100%)]' const panel = dark ? 'border-white/10 bg-[#252525]' : 'border-[#cfd6e4] bg-[#e9edf4]' const textStrong = dark ? 'text-white' : 'text-[#111520]' const textMid = dark ? 'text-white/70' : 'text-[#4f596b]' @@ -1663,6 +1663,7 @@ function LocalPcWorkspaceMockup({ performDeleteTreeTarget, requestDeleteTreeTarget, pasteTreeClipboard, + moveTreeTarget, refreshModuleFiles } = useWorkspaceFileActions({ selectedModule, @@ -3681,7 +3682,7 @@ function LocalPcWorkspaceMockup({ {workspaceActive && (
void moveTreeTarget(sourcePath, sourceKind, targetFolder)} onCopyTreePath={() => void copyTreePathToClipboard()} onRenameTreeTarget={requestRenameTreeTarget} onDeleteTreeTarget={requestDeleteTreeTarget} @@ -4112,6 +4114,7 @@ function LocalPcWorkspaceMockup({ )} {worldEditorOpen && ( { setWorldLayoutVersion((version) => version + 1) @@ -5826,12 +5829,12 @@ export default function App(): React.JSX.Element { const controlStatus = platform.control?.status const copy = controlStatus === 'QUEUED' ? { - title: `${platform.title} 대기 예약 취소 중`, - description: '대기열에서 요청을 제거하고 플랫폼 상태를 다시 확인하고 있습니다.' + title: t('{{platform}} 대기 예약 취소 중', { platform: platform.title }), + description: t('대기열에서 요청을 제거하고 플랫폼 상태를 다시 확인하고 있습니다.') } : { - title: `${platform.title} 제어권 요청 중`, - description: '플랫폼 사용 가능 여부와 대기열 상태를 확인하고 있습니다.' + title: t('{{platform}} 제어권 요청 중', { platform: platform.title }), + description: t('플랫폼 사용 가능 여부와 대기열 상태를 확인하고 있습니다.') } const loadingStartedAt = Date.now() @@ -5846,12 +5849,14 @@ export default function App(): React.JSX.Element { shouldWaitForControlPaint = true } if (result.method === 'DELETE') { - showTitlebarNotice(`${platform.title} 대기 예약이 취소되었습니다`, 'info', true) + showTitlebarNotice(t('{{platform}} 대기 예약이 취소되었습니다', { platform: platform.title }), 'info', true) } else if (payload?.result === 'GRANTED' || payload?.result === 'ALREADY_GRANTED') { - showTitlebarNotice(`${platform.title} 제어권이 부여되었습니다`, 'info', true) + showTitlebarNotice(t('{{platform}} 제어권이 부여되었습니다', { platform: platform.title }), 'info', true) } else if (payload?.result === 'QUEUED') { const position = payload.queue?.position - showTitlebarNotice(`${platform.title} 대기 예약되었습니다${position ? ` (${position}번째)` : ''}`, 'info', true) + showTitlebarNotice(position + ? t('{{platform}} 대기 예약되었습니다 ({{position}}번째)', { platform: platform.title, position }) + : t('{{platform}} 대기 예약되었습니다', { platform: platform.title }), 'info', true) } } catch { // Keep the existing curriculum visible when a control request fails. diff --git a/src/renderer/src/components/AppStatusModals.tsx b/src/renderer/src/components/AppStatusModals.tsx index 370a514..a19d1f4 100644 --- a/src/renderer/src/components/AppStatusModals.tsx +++ b/src/renderer/src/components/AppStatusModals.tsx @@ -166,7 +166,7 @@ export function RunningPlatformCloseModal({ title={t("개발 스튜디오 종료")} message={t("{{v0}} 개발 스튜디오를 종료할까요?", { v0: platformTitle === '로컬 PC' ? t('로컬 PC') : platformTitle })} detail={t("현재 열린 개발 스튜디오 세션만 종료합니다. 플랫폼 제어권은 반납되지 않습니다.")} - confirmLabel={t("종료")} + confirmLabel={t("확인")} onCancel={onCancel} onConfirm={onConfirm} /> diff --git a/src/renderer/src/components/TitleBar.tsx b/src/renderer/src/components/TitleBar.tsx index 31a5bdd..3e1f03e 100644 --- a/src/renderer/src/components/TitleBar.tsx +++ b/src/renderer/src/components/TitleBar.tsx @@ -1046,7 +1046,7 @@ export default function TitleBar({ user, accessToken = '', onHelpClick, onOpenCo type="button" onClick={handleConfirmClose} className="cursor-pointer rounded-md border border-red-400/35 bg-red-500/15 px-5 py-2 text-[12px] font-bold text-red-100 transition-colors hover:border-red-400/50 hover:bg-red-500/24" - >{t("종료")} + >{t("앱 종료")}
diff --git a/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx b/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx index f69b401..b749862 100644 --- a/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx +++ b/src/renderer/src/features/curriculum/CurriculumHomeScreen.tsx @@ -70,7 +70,7 @@ export default function CurriculumHomeScreen({
-
+

{t("주차별 커리큘럼")}

{curriculumSummary}

diff --git a/src/renderer/src/features/curriculum/LearningInfoCard.tsx b/src/renderer/src/features/curriculum/LearningInfoCard.tsx index c6b7b20..77360db 100644 --- a/src/renderer/src/features/curriculum/LearningInfoCard.tsx +++ b/src/renderer/src/features/curriculum/LearningInfoCard.tsx @@ -84,7 +84,7 @@ export default function LearningInfoCard({ disabled={studioDisabled} onClick={onStartStudio} className={[ - 'flex h-[39px] w-full items-center justify-center gap-2 rounded-md border-0 text-[14px] font-bold transition-all duration-150 ease-out', + 'flex h-[39px] w-full items-center justify-center gap-2 rounded-md border-0 text-[14px] font-bold leading-none transition-all duration-150 ease-out', studioDisabled ? 'cursor-not-allowed bg-[#d8dce5] text-[#8a91a0]' : 'cursor-pointer bg-[#714ef6] text-white shadow-[0_8px_18px_rgba(113,78,246,0.20)] hover:-translate-y-px hover:bg-[#6040df] hover:shadow-[0_10px_20px_rgba(113,78,246,0.26)] active:translate-y-0 active:shadow-[0_6px_14px_rgba(113,78,246,0.18)]' @@ -97,7 +97,7 @@ export default function LearningInfoCard({ disabled={!hasMaterials} onClick={onOpenMaterials} className={[ - 'mt-2.5 flex h-[39px] w-full items-center justify-center gap-2 rounded-md border text-[14px] font-bold transition-all duration-150 ease-out', + 'mt-2.5 flex h-[39px] w-full items-center justify-center gap-2 rounded-md border text-[14px] font-bold leading-none transition-all duration-150 ease-out', hasMaterials ? 'cursor-pointer border-[#714ef6] bg-white text-[#714ef6] hover:-translate-y-px hover:bg-[#f5f2ff] hover:shadow-[0_8px_16px_rgba(113,78,246,0.13)] active:translate-y-0 active:shadow-none' : 'cursor-not-allowed border-[#d8dce5] bg-[#f3f4f7] text-[#9aa1af]' diff --git a/src/renderer/src/features/platforms/PlatformCards.tsx b/src/renderer/src/features/platforms/PlatformCards.tsx index c0459f3..d913921 100644 --- a/src/renderer/src/features/platforms/PlatformCards.tsx +++ b/src/renderer/src/features/platforms/PlatformCards.tsx @@ -31,7 +31,7 @@ export function RunningPlatformCard({
-

{platform.platformKey === 'futuredrive-local-pc' ? t('로컬 PC') : platform.title}

+

{platform.platformKey === 'futuredrive-local-pc' ? t('시뮬레이션') : platform.title}

{weekTitle}

@@ -103,7 +103,7 @@ export function PlatformOptionCard({
-

{platform.platformKey === 'futuredrive-local-pc' ? t('로컬 PC') : platform.title}

+

{platform.platformKey === 'futuredrive-local-pc' ? t('시뮬레이션') : platform.title}

{platform.description}

@@ -142,9 +142,9 @@ export function PlatformOptionCard({ + {t("설정")}
) diff --git a/src/renderer/src/features/workspace/ModuleListHeader.tsx b/src/renderer/src/features/workspace/ModuleListHeader.tsx index 067077f..8bb25ba 100644 --- a/src/renderer/src/features/workspace/ModuleListHeader.tsx +++ b/src/renderer/src/features/workspace/ModuleListHeader.tsx @@ -8,7 +8,7 @@ type ModuleListHeaderProps = { export default function ModuleListHeader({ dark }: ModuleListHeaderProps): React.JSX.Element { useLocaleRender() return ( -
+
{t("상태")} {t("분류")} {t("모듈명")} diff --git a/src/renderer/src/features/workspace/ModuleListRow.tsx b/src/renderer/src/features/workspace/ModuleListRow.tsx index 4e105ae..12581aa 100644 --- a/src/renderer/src/features/workspace/ModuleListRow.tsx +++ b/src/renderer/src/features/workspace/ModuleListRow.tsx @@ -192,7 +192,7 @@ export default function ModuleListRow({ useEffect(() => { if (!busy) setProgress('') }, [busy]) const stateStyle: Record = { running: dark ? 'bg-[#12372d] text-[#72e2b5]' : 'bg-[#dff8ed] text-[#177553]', - stopped: dark ? 'bg-white/8 text-white/45' : 'bg-[#dce2eb] text-[#3e4c62]', + stopped: dark ? 'bg-white/8 text-white/45' : 'bg-[#edf0f4] text-[#3e4c62]', uninstalled: dark ? 'bg-[#2d2d2d] text-white/45' : 'bg-[#e7deef] text-[#60486f]' } const controlActions: Array<{ @@ -300,7 +300,7 @@ export default function ModuleListRow({ {module.cpu.toFixed(module.cpu >= 10 ? 0 : 1)}% - + @@ -314,7 +314,7 @@ export default function ModuleListRow({ {module.memory.toFixed(module.memory >= 10 ? 0 : 1)}% - + @@ -337,7 +337,7 @@ export default function ModuleListRow({ 'group/action relative grid h-7 w-7 cursor-pointer place-items-center rounded-md border transition', dark ? 'border-[#2f8dff]/35 bg-[#2f8dff]/12 text-[#9cc8ff] hover:bg-[#2f8dff]/22' - : 'border-[#2f8dff]/28 bg-[#edf6ff] text-[#1d5fae] hover:bg-[#dfefff]' + : 'border-[#a78bfa] bg-[#faf7ff] text-[#7c3aed] hover:border-[#7c3aed] hover:bg-[#f3efff]' ].join(' ')} > diff --git a/src/renderer/src/features/workspace/ModuleListToolbar.tsx b/src/renderer/src/features/workspace/ModuleListToolbar.tsx index e004275..685282f 100644 --- a/src/renderer/src/features/workspace/ModuleListToolbar.tsx +++ b/src/renderer/src/features/workspace/ModuleListToolbar.tsx @@ -75,7 +75,7 @@ export default function ModuleListToolbar({ ] return ( -
+
{FILTER_TABS.map(({ id, label }) => { const active = filter === id @@ -90,10 +90,10 @@ export default function ModuleListToolbar({ active ? dark ? 'border-white/10 bg-[#2b2b2b] text-white' - : 'border-[#c4b5df] bg-[#e2e8f0] text-[#38205b]' + : 'border-[#cfc2e1] bg-[#f8f6fb] text-[#38205b]' : dark ? 'border-white/8 bg-[#202020] text-white/45 hover:text-white/70' - : 'border-[#d4c9e4] bg-[#eee8f6] text-[#475569] hover:bg-[#e8def3] hover:text-[#38205b]' + : 'border-[#d8cee6] bg-white text-[#475569] hover:bg-[#f7f4fa] hover:text-[#38205b]' ].join(' ')} > {t(label)} diff --git a/src/renderer/src/features/workspace/ModuleListView.tsx b/src/renderer/src/features/workspace/ModuleListView.tsx index a8d8d65..3662acf 100644 --- a/src/renderer/src/features/workspace/ModuleListView.tsx +++ b/src/renderer/src/features/workspace/ModuleListView.tsx @@ -259,7 +259,7 @@ export default function ModuleListView({ return (
-
+
{t("모듈 리스트")} {t("자율주행 알고리즘 모듈 관리")} diff --git a/src/renderer/src/features/workspace/WorkspaceEditorPane.tsx b/src/renderer/src/features/workspace/WorkspaceEditorPane.tsx index 28445ce..c89b427 100644 --- a/src/renderer/src/features/workspace/WorkspaceEditorPane.tsx +++ b/src/renderer/src/features/workspace/WorkspaceEditorPane.tsx @@ -52,7 +52,7 @@ function WorkspaceEditorPane({
- {t("파일을 선택하세요")} + {t("파일을 선택하세요")}
) : activeFileIsImage ? ( diff --git a/src/renderer/src/features/workspace/WorkspaceEditorTabs.tsx b/src/renderer/src/features/workspace/WorkspaceEditorTabs.tsx index f26b6b5..31296c0 100644 --- a/src/renderer/src/features/workspace/WorkspaceEditorTabs.tsx +++ b/src/renderer/src/features/workspace/WorkspaceEditorTabs.tsx @@ -38,7 +38,7 @@ function WorkspaceEditorTabs({ if (files.length === 0) return null return ( -
+
{files.map((file, index) => (
@@ -326,9 +326,7 @@ export default function WorkspaceRosConnectionsView({
{t("토픽 연결")} - {t("발행 노드에서 구독 노드로 전달")}
- {activeConnections}/{connections.length}{t("활성")}
{visibleConnections.length ? visibleConnections.map((connection) => { @@ -367,7 +365,7 @@ export default function WorkspaceRosConnectionsView({ ) }) : ( -
{rosGraphLoading ? t("조회 중...") : rosGraph.message ? t("ROS 그래프 조회에 실패했습니다.") : activeOnly ? t("활성 연결이 없습니다.") : t("조회된 ROS 토픽이 없습니다.")}
+
{rosGraphLoading ? t("조회 중...") : rosGraph.message ? t("ROS 그래프 조회에 실패했습니다.") : activeOnly ? t("활성 연결이 없습니다.") : t("조회된 ROS 토픽이 없습니다.")}
)}
diff --git a/src/renderer/src/features/workspace/WorkspaceSourceControlView.tsx b/src/renderer/src/features/workspace/WorkspaceSourceControlView.tsx index 1742df7..7926009 100644 --- a/src/renderer/src/features/workspace/WorkspaceSourceControlView.tsx +++ b/src/renderer/src/features/workspace/WorkspaceSourceControlView.tsx @@ -148,7 +148,7 @@ function WorkspaceSourceControlView({ style={{ gridColumn: '4 / span 3', gridRow: 2 }} >
-
+
{[ { key: 'source' as const, label: t("소스 제어") }, @@ -163,10 +163,10 @@ function WorkspaceSourceControlView({ gitPanelTab === tab.key ? dark ? 'border-white/10 bg-[#2b2b2b] text-white' - : 'border-[#c4b5df] bg-[#e2e8f0] text-[#38205b]' + : 'border-[#cfc2e1] bg-[#f8f6fb] text-[#38205b]' : dark ? 'border-white/8 bg-[#202020] text-white/45 hover:text-white/70' - : 'border-[#d4c9e4] bg-[#eee8f6] text-[#475569] hover:bg-[#e8def3] hover:text-[#38205b]' + : 'border-[#d8cee6] bg-white text-[#475569] hover:bg-[#f7f4fa] hover:text-[#38205b]' ].join(' ')} > {t(tab.label)} diff --git a/src/renderer/src/features/workspace/WorldEditorModal.tsx b/src/renderer/src/features/workspace/WorldEditorModal.tsx index 74b059f..70969cb 100644 --- a/src/renderer/src/features/workspace/WorldEditorModal.tsx +++ b/src/renderer/src/features/workspace/WorldEditorModal.tsx @@ -10,6 +10,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js' type WorldObjectType = WorldLayoutObject['type'] type WorldEditorModalProps = { + dark: boolean workspaceRoot: string onClose: () => void onSaved?: (layout: WorldLayout) => void @@ -173,7 +174,7 @@ async function loadWorldSdf(parent: THREE.Group): Promise { } } -export default function WorldEditorModal({ workspaceRoot, onClose, onSaved }: WorldEditorModalProps): React.JSX.Element { +export default function WorldEditorModal({ dark, workspaceRoot, onClose, onSaved }: WorldEditorModalProps): React.JSX.Element { useLocaleRender() const canvasHostRef = useRef(null) const viewerRef = useRef(null) @@ -436,32 +437,32 @@ export default function WorldEditorModal({ workspaceRoot, onClose, onSaved }: Wo } return ( -
-
-
+
+
+
-

{t("맵 설정")}

-

{t("주차 워크스페이스에 월드 배치를 저장합니다.")}

+

{t("맵 설정")}

+

{t("주차 워크스페이스에 월드 배치를 저장합니다.")}

- +
-