Refactor: 모델 관리 페이지 리팩토링
This commit is contained in:
parent
a91c74b42f
commit
f0394e0977
55
frontend/src/components/ModelManage/EvaluationTab.tsx
Normal file
55
frontend/src/components/ModelManage/EvaluationTab.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import ModelBarChart from '@/components/ModelBarChart';
|
||||
|
||||
interface EvaluationTabProps {
|
||||
selectedModel: string | null;
|
||||
setSelectedModel: (model: string | null) => void;
|
||||
}
|
||||
|
||||
export default function EvaluationTab({ selectedModel, setSelectedModel }: EvaluationTabProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="select-model">모델 선택</Label>
|
||||
<Select onValueChange={setSelectedModel}>
|
||||
<SelectTrigger id="select-model">
|
||||
<SelectValue placeholder="모델을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="genesis">Genesis</SelectItem>
|
||||
<SelectItem value="explorer">Explorer</SelectItem>
|
||||
<SelectItem value="quantum">Quantum</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedModel && (
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-6">
|
||||
<ModelBarChart
|
||||
data={[
|
||||
{ name: 'precision', value: 0.734, fill: 'var(--color-precision)' },
|
||||
{ name: 'recall', value: 0.75, fill: 'var(--color-recall)' },
|
||||
{ name: 'mAP50', value: 0.995, fill: 'var(--color-map50)' },
|
||||
{ name: 'mAP50_95', value: 0.97, fill: 'var(--color-map50-95)' },
|
||||
{ name: 'fitness', value: 0.973, fill: 'var(--color-fitness)' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<LabelingPreview />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LabelingPreview() {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-lg border bg-white p-4">
|
||||
<p>레이블링 프리뷰</p>
|
||||
</div>
|
||||
);
|
||||
}
|
189
frontend/src/components/ModelManage/SettingsForm.tsx
Normal file
189
frontend/src/components/ModelManage/SettingsForm.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface SettingsFormProps {
|
||||
projectId: string | null; // projectId를 프랍으로 받음
|
||||
onSubmit?: (data: SettingsFormData) => void;
|
||||
}
|
||||
|
||||
export interface SettingsFormData {
|
||||
projectId: number | null;
|
||||
selectedModel: string | null;
|
||||
ratio: number;
|
||||
epochs: number;
|
||||
batchSize: number;
|
||||
optimizer: string;
|
||||
lr0: number;
|
||||
lrf: number;
|
||||
}
|
||||
|
||||
export default function SettingsForm({ projectId, onSubmit }: SettingsFormProps) {
|
||||
const numericProjectId = projectId ? parseInt(projectId, 10) : null;
|
||||
|
||||
const { data: models } = useProjectModelsQuery(numericProjectId ?? 0);
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
||||
const [ratio, setRatio] = useState<number>(0.8);
|
||||
const [epochs, setEpochs] = useState<number>(50);
|
||||
const [batchSize, setBatchSize] = useState<number>(32);
|
||||
const [optimizer, setOptimizer] = useState<string>('SGD');
|
||||
const [lr0, setLr0] = useState<number>(0.01);
|
||||
const [lrf, setLrf] = useState<number>(0.001);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (onSubmit) {
|
||||
onSubmit({
|
||||
projectId: numericProjectId,
|
||||
selectedModel,
|
||||
ratio,
|
||||
epochs,
|
||||
batchSize,
|
||||
optimizer,
|
||||
lr0,
|
||||
lrf,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="grid w-full gap-6"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<fieldset className="grid gap-6 rounded-lg border p-4">
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">모델 설정</legend>
|
||||
|
||||
{/* 모델 선택 */}
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="model">모델 선택</Label>
|
||||
<Select onValueChange={setSelectedModel}>
|
||||
<SelectTrigger id="model">
|
||||
<SelectValue placeholder="모델을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models?.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.name}
|
||||
>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 훈련/검증 비율 및 학습 파라미터 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputWithLabel
|
||||
label="훈련/검증 비율"
|
||||
placeholder="예: 0.8 (80% 훈련, 20% 검증)"
|
||||
id="ratio"
|
||||
value={ratio}
|
||||
onChange={(e) => setRatio(parseFloat(e.target.value))}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="에포크 수"
|
||||
placeholder="예: 50 (총 반복 횟수)"
|
||||
id="epochs"
|
||||
value={epochs}
|
||||
onChange={(e) => setEpochs(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="Batch 크기"
|
||||
placeholder="예: 32 (한번에 처리할 샘플 수)"
|
||||
id="batch"
|
||||
value={batchSize}
|
||||
onChange={(e) => setBatchSize(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<SelectWithLabel
|
||||
label="옵티마이저"
|
||||
id="optimizer"
|
||||
options={['SGD', 'Adam', 'AdamW', 'NAdam', 'RAdam', 'RMSProp']}
|
||||
value={optimizer}
|
||||
onChange={setOptimizer}
|
||||
placeholder="옵티마이저 선택"
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="학습률(LR0)"
|
||||
placeholder="예: 0.01 (초기 학습률)"
|
||||
id="lr0"
|
||||
value={lr0}
|
||||
onChange={(e) => setLr0(parseFloat(e.target.value))}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="최종 학습률(LRF)"
|
||||
placeholder="예: 0.001 (최종 학습률)"
|
||||
id="lrf"
|
||||
value={lrf}
|
||||
onChange={(e) => setLrf(parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
>
|
||||
설정 저장
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
interface InputWithLabelProps {
|
||||
label: string;
|
||||
id: string;
|
||||
placeholder: string;
|
||||
value: number;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
function InputWithLabel({ label, id, placeholder, value, onChange }: InputWithLabelProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<Input
|
||||
id={id}
|
||||
type="number"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectWithLabelProps {
|
||||
label: string;
|
||||
id: string;
|
||||
options: string[];
|
||||
placeholder: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function SelectWithLabel({ label, id, options, placeholder, onChange }: SelectWithLabelProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<Select onValueChange={onChange}>
|
||||
<SelectTrigger id={id}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
45
frontend/src/components/ModelManage/TrainingTab.tsx
Normal file
45
frontend/src/components/ModelManage/TrainingTab.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import ModelLineChart from '@/components/ModelLineChart';
|
||||
import SettingsForm from './SettingsForm';
|
||||
|
||||
interface TrainingTabProps {
|
||||
training: boolean;
|
||||
handleTrainingToggle: () => void;
|
||||
trainingDataList: {
|
||||
epoch: number;
|
||||
box_loss: number;
|
||||
cls_loss: number;
|
||||
dfl_loss: number;
|
||||
fitness: number;
|
||||
}[];
|
||||
projectId: string | null; // projectId를 프랍으로 받음
|
||||
}
|
||||
|
||||
export default function TrainingTab({ training, handleTrainingToggle, trainingDataList, projectId }: TrainingTabProps) {
|
||||
return (
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsForm projectId={projectId} />
|
||||
<Button
|
||||
variant={training ? 'destructive' : 'outlinePrimary'}
|
||||
size="lg"
|
||||
onClick={handleTrainingToggle}
|
||||
>
|
||||
{training ? '학습 중단' : '학습 시작'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<ModelLineChart
|
||||
data={trainingDataList.map((data) => ({
|
||||
epoch: data.epoch.toString(),
|
||||
loss1: data.box_loss,
|
||||
loss2: data.cls_loss,
|
||||
loss3: data.dfl_loss,
|
||||
fitness: data.fitness,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
66
frontend/src/components/ModelManage/index.tsx
Normal file
66
frontend/src/components/ModelManage/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import useTrainWebSocket from '@/hooks/useTrainPolling';
|
||||
import useTrainStore from '@/stores/useTrainStore';
|
||||
import TrainingTab from './TrainingTab';
|
||||
import EvaluationTab from './EvaluationTab';
|
||||
|
||||
export default function ModelManage() {
|
||||
const { projectId } = useParams<{ projectId?: string }>();
|
||||
const [training, setTraining] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
||||
|
||||
const numericProjectId = projectId ?? null;
|
||||
|
||||
useTrainWebSocket(training, numericProjectId);
|
||||
|
||||
const { trainingDataList } = useTrainStore((state) => ({
|
||||
trainingDataList: numericProjectId ? state.trainingDataByProject[numericProjectId] || [] : [],
|
||||
}));
|
||||
|
||||
const handleTrainingToggle = () => {
|
||||
setTraining((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid h-screen w-full">
|
||||
<div className="flex flex-col">
|
||||
<header className="bg-background sticky top-0 z-10 flex h-[57px] items-center gap-1 border-b px-4">
|
||||
<h1 className="text-xl font-semibold">모델 관리</h1>
|
||||
</header>
|
||||
|
||||
<main className="grid flex-1 gap-4 overflow-auto p-4">
|
||||
<Tabs
|
||||
defaultValue="train"
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="train">모델 학습</TabsTrigger>
|
||||
<TabsTrigger value="results">모델 평가</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 학습 탭 */}
|
||||
<TabsContent value="train">
|
||||
<TrainingTab
|
||||
training={training}
|
||||
handleTrainingToggle={handleTrainingToggle}
|
||||
trainingDataList={trainingDataList}
|
||||
projectId={numericProjectId}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* 평가 탭 */}
|
||||
<TabsContent value="results">
|
||||
<EvaluationTab
|
||||
selectedModel={selectedModel}
|
||||
setSelectedModel={setSelectedModel}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user