Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#134] 대회 페이지 시뮬레이션 영역 모달로 분리하기 #146

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions frontend/src/components/Common/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { css, cx } from '@style/css';

import type { HTMLAttributes, MouseEvent } from 'react';
import { useContext, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';

import { ModalContext } from './ModalContext';
import { ModalProvider } from './ModalProvider';

export interface Props extends HTMLAttributes<HTMLDialogElement> {
onBackdropPressed?: () => void;
}

export function Modal({ onBackdropPressed, children, ...props }: Props) {
const modal = useContext(ModalContext);
const $dialog = useRef<HTMLDialogElement>(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에서 dialog 앞에 $을 붙이신 이유가 뭔가요?:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOM을 나타내는 경우엔 $를 붙여서 구분짓고 있습니다. 개인적인 코딩 습관? 이에요

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오오 감사합니다


const handleClickBackdrop = (e: MouseEvent<HTMLDialogElement>) => {
const $target = e.target as HTMLDialogElement;

if ($target.nodeName !== 'DIALOG') return;

if (onBackdropPressed instanceof Function) {
onBackdropPressed();
}
Comment on lines +23 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common 아래에 모달이라 onBackdropPressed가 있을수도 있고 없을 수도 있다는 고 있으면 실행해라는 의미로 해석됩니다.

typeof onBackdropPressed === 'function'로 안 하신 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별 의미는 없었습니다. typeof를 쓰는 것이 더 유리한가요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String으로 타입 비교 안 하고 'string'하는 것처럼 함수도 'function'으로 하는 게 일관성 있어 보여서 물어봤습니다. 찾아 봤는데, 아무런 문제 없어보이네요!

};

useEffect(() => {
if (modal.isOpen) {
$dialog.current?.showModal();
} else {
$dialog.current?.close();
}
}, [modal.isOpen]);

return ReactDOM.createPortal(
<dialog
ref={$dialog}
className={cx(style, dialogStyle)}
aria-modal="true"
aria-labelledby="dialog-title"
onClick={handleClickBackdrop}
{...props}
>
Comment on lines +36 to +44
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createPortal은 처음 보는데 이렇게도 쓸 수 있군요, 감사합니다!:D

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 app이 되는 <div id="root"> 밖에 컴포넌트를 추가하며 모달 구현에 많이 쓰입니다.

<div className={contentStyle}>{children}</div>
</dialog>,
document.body,
);
}

Modal.Context = ModalContext;
Modal.Provider = ModalProvider;

const style = css({
borderRadius: '0.5rem',
});

const dialogStyle = css({
position: 'fixed',
left: '50%',
top: '50%',
transform: 'translate(-50%,-50%)',
width: '500px',
height: '400px',
_backdrop: {
background: 'rgba(00,00,00,0.5)',
backdropFilter: 'blur(1rem)',
Comment on lines +65 to +67
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모달 만들때 꿀팁이네요.
backdrop-filter 속성 유용하네요

},
});

const contentStyle = css({
width: '100%',
height: '100%',
});
13 changes: 13 additions & 0 deletions frontend/src/components/Common/Modal/ModalContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContext } from 'react';

interface ModalContextProps {
isOpen: boolean;
close: () => void;
open: () => void;
}

export const ModalContext = createContext<ModalContextProps>({
isOpen: false,
close: () => {},
open: () => {},
});
26 changes: 26 additions & 0 deletions frontend/src/components/Common/Modal/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ReactNode } from 'react';
import { useState } from 'react';

import { ModalContext } from './ModalContext';

export function ModalProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const close = () => {
setIsOpen(false);
};
const open = () => {
setIsOpen(true);
};

return (
<ModalContext.Provider
value={{
isOpen,
close,
open,
}}
>
{children}
</ModalContext.Provider>
);
}
2 changes: 2 additions & 0 deletions frontend/src/components/Common/Modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Modal } from './Modal';
export type { Props as ModalProps } from './Modal';
1 change: 1 addition & 0 deletions frontend/src/components/Common/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Input } from './Input';
export * from './Modal';
46 changes: 46 additions & 0 deletions frontend/src/components/Simulation/SimulationInputModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useContext, useState } from 'react';

import type { SimulationInput } from '@/hooks/simulation';
import { deepCopy } from '@/utils/copy';

import { Modal, type ModalProps } from '../Common';
import { SimulationInputList } from './SimulationInputList';

interface Props extends ModalProps {
simulationInputs: SimulationInput[];
onSave: (inputs: SimulationInput[]) => void;
}

export function SimulationInputModal({ simulationInputs, onSave, ...props }: Props) {
const modal = useContext(Modal.Context);
const [inputs, setInputs] = useState<SimulationInput[]>(deepCopy(simulationInputs));

const handleCloseModal = () => {
setInputs(simulationInputs);
modal.close();
};

const handleSave = () => {
onSave(deepCopy(inputs));
modal.close();
Comment on lines +20 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useContext안에 모달을 닫고 여는 함수들의 네이밍(close,open)이 부실하다 생각했는데
modal로 받고 modal.close라고 하니 깔끔하고 좋네요.

};

const handleChangeInput = (targetId: number, newParam: string) => {
const changedSimulation = inputs.find(({ id }) => id === targetId);
if (changedSimulation) {
changedSimulation.input = newParam;
}
setInputs([...inputs]);
};

return (
<Modal {...props}>
<SimulationInputList
inputList={inputs}
onChangeInput={handleChangeInput}
></SimulationInputList>
<button onClick={handleCloseModal}>닫기</button>
<button onClick={handleSave}>저장</button>
</Modal>
);
}
2 changes: 1 addition & 1 deletion frontend/src/hooks/simulation/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './useSimulations';
export * from './useSimulation';
export * from './types';
94 changes: 94 additions & 0 deletions frontend/src/hooks/simulation/useSimulation.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 변경사항은 useSimulations -> useSimulation으로 바뀌면서 생긴 것들이 다수고, 전체적으로 simulationInputs... 이렇게 시작하던 이름을 inputs 이렇게 바꾼 것들이 다에요

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect, useMemo, useState } from 'react';

import evaluator from '@/modules/evaluator';

import type { SimulationInput, SimulationResult } from './types';

export const useSimulation = () => {
const [inputs, setInputs] = useState<SimulationInput[]>([
{ id: 1, input: '' },
{ id: 2, input: '' },
{ id: 3, input: '' },
{ id: 4, input: '' },
{ id: 5, input: '' },
]);
const [results, setResults] = useState<SimulationResult[]>([
{ id: 1, isDone: true, input: '', output: '' },
{ id: 2, isDone: true, input: '', output: '' },
{ id: 3, isDone: true, input: '', output: '' },
{ id: 4, isDone: true, input: '', output: '' },
{ id: 5, isDone: true, input: '', output: '' },
]);
const isRunning = useMemo(() => {
return results.some((result) => !result.isDone);
}, [results]);

useEffect(() => {
return evaluator.subscribe(({ result: output, error, task }) => {
if (!task) return;

setResults((results) => {
return results.map((result) => {
if (result.id !== task.clientId) return result;

if (error) {
return {
...result,
isDone: true,
output: `${error.name}: ${error.message} \n${error.stack}`,
};
}
return {
...result,
isDone: true,
output,
};
});
});
});
}, []);

function run(code: string) {
const tasks = inputs.map(({ id, input }) => evaluator.createEvalMessage(id, code, input));

const isRequestSuccess = evaluator.evaluate(tasks);

if (!isRequestSuccess) {
return;
}

setResults((results) => {
return results
.map((result, index) => ({
...result,
input: inputs[index].input,
}))
.map(toEvaluatingState);
});
}

function changeInputs(inputs: SimulationInput[]) {
setInputs([...inputs]);
}

function cancel() {
evaluator.cancelEvaluation();
}

return {
inputs,
results,
isRunning,
run,
cancel,
changeInputs,
};
};

const toEvaluatingState = (simulation: SimulationResult) => {
return {
...simulation,
output: '계산중...',
isDone: false,
};
};
100 changes: 0 additions & 100 deletions frontend/src/hooks/simulation/useSimulations.ts

This file was deleted.

Loading
Loading