Skip to content

Commit

Permalink
feat: GLTFLoader의 프로그래스를 보여 줄 로딩 오버레이 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
chiabi committed Jul 23, 2024
1 parent 67d55ec commit ba37ff1
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 15 deletions.
30 changes: 21 additions & 9 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@ module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"prettier",
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
"react-refresh/only-export-components": [
"warn",
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
},
};
19 changes: 18 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { useState } from 'react';
import { ModelViewer } from './components/ModelViewer';
import { LoadingOverlay } from './components/LoadingOverlay';

function App() {
return <ModelViewer />;
const [progress, setProgress] = useState(0);
const [isLoading, setLoading] = useState(true);

const handleLoadComplete = () => {
setLoading(false);
};

return (
<>
<ModelViewer
onProgress={setProgress}
onLoadComplete={handleLoadComplete}
/>
<LoadingOverlay progress={progress} isLoading={isLoading} />
</>
);
}

export default App;
57 changes: 57 additions & 0 deletions src/components/LoadingOverlay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { act, render, screen } from '@testing-library/react';
import { expect, describe, it, vi } from 'vitest';
import { LoadingOverlay } from './LoadingOverlay';

describe('LoadingOverlay', () => {
it('renders correctly with given progress', () => {
render(<LoadingOverlay progress={50} isLoading={true} />);

expect(screen.getByRole('progressbar')).toBeInTheDocument();
expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('updates progress correctly', () => {
const { rerender } = render(
<LoadingOverlay progress={25} isLoading={true} />
);

let progressBar = screen.getByRole('progressbar').firstChild;
expect(progressBar).toHaveStyle('transform: translateX(-75%)');

rerender(<LoadingOverlay progress={75} isLoading={true} />);
progressBar = screen.getByRole('progressbar').firstChild;
expect(progressBar).toHaveStyle('transform: translateX(-25%)');
});

it('fades out and removes from DOM when not loading', async () => {
vi.useFakeTimers();
const { rerender } = render(
<LoadingOverlay progress={100} isLoading={true} />
);

expect(screen.getByRole('progressbar')).toBeInTheDocument();

rerender(<LoadingOverlay progress={100} isLoading={false} />);

expect(
screen.getByRole('progressbar').parentElement?.parentElement
).toHaveClass('opacity-0');

act(() => {
vi.advanceTimersByTime(350);
});

expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();

vi.useRealTimers();
});

it('has correct accessibility attributes', () => {
render(<LoadingOverlay progress={50} isLoading={true} />);

const progressBar = screen.getByRole('progressbar')
.firstChild as HTMLElement;
expect(progressBar).toHaveAttribute('aria-valuenow', '50');
expect(progressBar).toHaveAttribute('aria-labelledby');
});
});
52 changes: 52 additions & 0 deletions src/components/LoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useEffect, useId, useState, useTransition } from 'react';

interface LoadingOverlayProps {
progress: number;
isLoading: boolean;
}

export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({
progress,
isLoading,
}) => {
const id = useId();
const [_, startTransition] = useTransition();
const [show, setShow] = useState(isLoading);

useEffect(() => {
if (!isLoading) {
startTransition(() => {
setTimeout(() => setShow(false), 350);
});
} else {
setShow(true);
}
}, [isLoading]);

if (!show) {
return null;
}

return (
<div
className={`fixed top-0 w-screen h-screen flex items-center justify-center bg-gradient-to-b from-slate-950 to-yellow-950 transition-opacity duration-300 ${isLoading ? 'opacity-100' : 'opacity-0'}`}
>
<div className="w-1/3 flex flex-col items-center gap-4">
<div
className="w-full rounded-full h-2.5 bg-gray-700 overflow-hidden"
role="progressbar"
>
<div
className="w-full h-2.5 rounded-full bg-gradient-to-r from-yellow-700 to-yellow-200 transition-transform"
style={{ transform: `translateX(${progress - 100}%)` }}
aria-valuenow={progress}
aria-labelledby={id}
/>
</div>
<div id={id} className="font-bold text-white">
Loading...
</div>
</div>
</div>
);
};
18 changes: 15 additions & 3 deletions src/components/ModelViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ import {
} from '../three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

export const ModelViewer: React.FC = () => {
interface ModelViewerProps {
onProgress: (progress: number) => void;
onLoadComplete: () => void;
}

export const ModelViewer: React.FC<ModelViewerProps> = ({
onProgress,
onLoadComplete,
}) => {
const mountRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -21,12 +29,16 @@ export const ModelViewer: React.FC = () => {
const camera = createCamera();
const renderer = createRenderer();
const lights = createLights(scene);

const controls = new OrbitControls(camera, renderer.domElement);

mountRef.current.appendChild(renderer.domElement);

loadModel(scene, '/models/vending_machine.gltf');
loadModel(
scene,
'/models/vending_machine.gltf',
onProgress,
onLoadComplete
);

if (import.meta.env.DEV) {
import('../three/gui').then(({ setupGUI }) => {
Expand Down
10 changes: 8 additions & 2 deletions src/three/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { Mesh, Scene } from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { applyMaterials } from './materials';

export const loadModel = (scene: Scene, url: string): void => {
export const loadModel = (
scene: Scene,
url: string,
onProgress: (progress: number) => void,
onLoad: () => void
): void => {
const loader = new GLTFLoader();
loader.load(
url,
Expand All @@ -13,9 +18,10 @@ export const loadModel = (scene: Scene, url: string): void => {
}
});
scene.add(gltf.scene);
onLoad();
},
(progress) => {
console.log((progress.loaded / progress.total) * 100 + '% loaded');
onProgress((progress.loaded / progress.total) * 100);
},
(error) => {
console.error('An error happened', error);
Expand Down

0 comments on commit ba37ff1

Please sign in to comment.