Skip to content

Commit 9b2eb79

Browse files
committed
feat: job 진행율 기능 및 UI 추가
1 parent 9be34b4 commit 9b2eb79

File tree

14 files changed

+170
-17
lines changed

14 files changed

+170
-17
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ jobs:
4646
export NGINX_CONF=react_nginx.conf
4747
docker compose -f docker-compose.yml -f docker-compose.build.yml build
4848
49+
echo "🗄️ DB 마이그레이션 실행..."
50+
# 인자 없이 실행 → 미적용된 모든 migration을 자동으로 반영
51+
docker compose -f docker-compose.yml -f docker-compose.build.yml \
52+
run --rm cvat_server python manage.py migrate
53+
4954
echo "🚀 Starting services..."
5055
export CVAT_HOST=172.16.0.172
5156
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d

.github/workflows/deploy-single.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,14 @@ jobs:
147147
-f docker-compose.single.yml \
148148
pull
149149
150-
echo "🚀 서비스 기동..."
150+
echo "�️ DB 마이그레이션 실행..."
151+
# 인자 없이 실행 → 미적용된 모든 migration을 자동으로 반영
152+
docker compose \
153+
-f docker-compose.yml \
154+
-f docker-compose.single.yml \
155+
run --rm cvat_server python manage.py migrate
156+
157+
echo "�🚀 서비스 기동..."
151158
docker compose \
152159
-f docker-compose.yml \
153160
-f docker-compose.single.yml \

cvat-core/src/server-response-types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ export interface SerializedJob {
156156
frame_count: number;
157157
start_frame: number;
158158
stop_frame: number;
159+
// Save 버튼 클릭 시 저장된 마지막 프레임 번호. null이면 아직 Save한 적 없음
160+
last_frame?: number | null;
159161
task_id: number;
160162
task_name: string;
161163
updated_date: string;
@@ -591,5 +593,5 @@ export interface SerializedTaskValidationLayout extends SerializedJobValidationL
591593
disabled_frames?: number[];
592594
}
593595

594-
export interface APIOrganizationMembersFilter extends APICommonFilterParams {}
596+
export interface APIOrganizationMembersFilter extends APICommonFilterParams { }
595597
export type OrganizationMembersFilter = Camelized<APIOrganizationMembersFilter>;

cvat-core/src/session-implementation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export function implementJob(Job: typeof JobClass): typeof JobClass {
7676
...('assignee' in fields ? { assignee: fields.assignee } : {}),
7777
...('stage' in fields ? { stage: fields.stage } : {}),
7878
...('state' in fields ? { state: fields.state } : {}),
79+
// Save 시점의 마지막 프레임 번호 - serverProxy에 전달하여 BE에 저장
80+
...('last_frame' in fields ? { last_frame: fields.last_frame } : {}),
7981
};
8082

8183
if (jobData.assignee) {
@@ -790,7 +792,7 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass {
790792
const { taskID, rqID } = await serverProxy.tasks.create(
791793
taskSpec,
792794
taskDataSpec,
793-
options?.updateStatusCallback || (() => {}),
795+
options?.updateStatusCallback || (() => { }),
794796
);
795797

796798
await requestsManager.listen(rqID, {

cvat-core/src/session.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,8 @@ export class Job extends Session {
536536
target_storage: Storage,
537537
parent_job_id: number | null;
538538
consensus_replicas: number;
539+
// Save 버튼 클릭 시 FE에서 BE로 전달되는 마지막 프레임 번호
540+
last_frame: number | null;
539541
};
540542

541543
constructor(initialData: InitializerType) {
@@ -566,6 +568,8 @@ export class Job extends Session {
566568
target_storage: undefined,
567569
parent_job_id: null,
568570
consensus_replicas: undefined,
571+
// Save하기 전에는 null로 초기화
572+
last_frame: null,
569573
};
570574

571575
this.#data.id = initialData.id ?? this.#data.id;
@@ -583,6 +587,10 @@ export class Job extends Session {
583587
this.#data.created_date = initialData.created_date ?? this.#data.created_date;
584588
this.#data.parent_job_id = initialData.parent_job_id ?? this.#data.parent_job_id;
585589
this.#data.consensus_replicas = initialData.consensus_replicas ?? this.#data.consensus_replicas;
590+
// last_frame은 undefined는 무시, null은 '새 저장 없음' 의미로 유지
591+
if (initialData.last_frame !== undefined) {
592+
this.#data.last_frame = initialData.last_frame;
593+
}
586594

587595
if (Array.isArray(initialData.labels)) {
588596
this.#data.labels = initialData.labels.map((labelData) => {
@@ -637,6 +645,10 @@ export class Job extends Session {
637645
this.#data.guide_id = data.guide_id ?? this.#data.guide_id;
638646
this.#data.updated_date = data.updated_date ?? this.#data.updated_date;
639647
this.#data.bug_tracker = data.bug_tracker ?? this.#data.bug_tracker;
648+
// reinit 시에도 last_frame 동기화 (save 후 응답에서 갱신)
649+
if (data.last_frame !== undefined) {
650+
this.#data.last_frame = data.last_frame;
651+
}
640652

641653
// TODO: labels also may get changed, but it will affect many code within the application
642654
// so, need to think on this additionally
@@ -730,6 +742,21 @@ export class Job extends Session {
730742
return this.#data.updated_date;
731743
}
732744

745+
// Save 버튼으로 저장된 마지막 프레임 번호. null이면 아직 Save한 적 없는 Job
746+
public get lastFrame(): number | null {
747+
return this.#data.last_frame;
748+
}
749+
750+
// last_frame 기반으로 계산한 진행율 (0~100, 정수). null이면 미표시
751+
public get annotationProgress(): number | null {
752+
if (this.#data.last_frame == null) return null;
753+
const totalFrames = this.#data.stop_frame - this.#data.start_frame + 1;
754+
if (totalFrames <= 0) return null;
755+
return Math.round(
756+
((this.#data.last_frame - this.#data.start_frame + 1) / totalFrames) * 100
757+
);
758+
}
759+
733760
public get sourceStorage(): Storage {
734761
return this.#data.source_storage;
735762
}
@@ -955,6 +982,8 @@ export class Task extends Session {
955982
source_storage: initialData.source_storage,
956983
parent_job_id: job.parent_job_id,
957984
consensus_replicas: job.consensus_replicas,
985+
// Task API 응답의 embedded jobs에도 last_frame 전달 (job-item 진행율 표시용)
986+
last_frame: job.last_frame,
958987
});
959988
data.jobs.push(jobInstance);
960989
}

cvat-ui/src/actions/annotation-actions.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1055,9 +1055,15 @@ export function saveAnnotationsAsync(): ThunkAction {
10551055
await saveJobEvent.close();
10561056
dispatch(saveLogsAsync());
10571057

1058+
// Save 시점의 현재 프레임 번호를 Redux store에서 읽어 last_frame으로 저장
1059+
const currentFrameNumber = getStore().getState().annotation.player.frame.number;
1060+
const fieldsToUpdate: Record<string, any> = { last_frame: currentFrameNumber };
1061+
1062+
// Job이 NEW 상태이면 IN_PROGRESS로 함께 변경 (API 호출 1회로 통합)
10581063
if (jobInstance instanceof cvat.classes.Job && jobInstance.state === cvat.enums.JobState.NEW) {
1059-
await dispatch(updateJobAsync(jobInstance, { state: JobState.IN_PROGRESS }));
1064+
fieldsToUpdate.state = JobState.IN_PROGRESS;
10601065
}
1066+
await dispatch(updateJobAsync(jobInstance, fieldsToUpdate));
10611067

10621068
dispatch({
10631069
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,

cvat-ui/src/components/job-item/job-item.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
LoadingOutlined, MoreOutlined, QuestionCircleOutlined,
2121
} from '@ant-design/icons/lib/icons';
2222
import { DurationIcon, FramesIcon } from 'icons';
23+
import Progress from 'antd/lib/progress';
2324
import {
2425
Job, JobStage, JobState, JobType, Task, User,
2526
} from 'cvat-core-wrapper';
@@ -55,7 +56,7 @@ function ReviewSummaryComponent({ jobInstance }: Readonly<{ jobInstance: Job }>)
5556
useEffect(() => {
5657
setError(null);
5758
jobInstance
58-
.issues(jobInstance.id)
59+
.issues()
5960
.then((issues: any[]) => {
6061
if (isMounted()) {
6162
setSummary({
@@ -284,6 +285,28 @@ function JobItem(props: Readonly<Props>): JSX.Element {
284285
</Row>
285286
</Col>
286287
</Row>
288+
{/* Save 이력이 있을 때만 하단 progress bar 섹션 표시 */}
289+
{job.annotationProgress != null && (
290+
<div className='cvat-job-item-progress-section'>
291+
<Row justify='space-between' align='middle'>
292+
<Col>
293+
<Text type='secondary'>
294+
{/* 마지막 작업 프레임 번호와 전체 프레임 범위를 함께 표시 */}
295+
{`Frame ${job.lastFrame} / ${job.stopFrame}`}
296+
</Text>
297+
</Col>
298+
<Col>
299+
<Text type='secondary'>{`${job.annotationProgress}%`}</Text>
300+
</Col>
301+
</Row>
302+
<Progress
303+
percent={job.annotationProgress}
304+
showInfo={false}
305+
strokeColor='#1890ff'
306+
size='small'
307+
/>
308+
</div>
309+
)}
287310
<div
288311
onClick={handleContextMenuClick}
289312
className='cvat-job-item-more-button cvat-actions-menu-button'

cvat-ui/src/components/job-item/styles.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@
8585
top: $grid-unit-size * 6.5;
8686
}
8787

88+
// Save 이력이 있을 때 표시되는 하단 progress bar 섹션
89+
.cvat-job-item-progress-section {
90+
margin-top: $grid-unit-size * 1.5;
91+
92+
.ant-progress {
93+
margin-top: $grid-unit-size * 0.5;
94+
}
95+
}
96+
8897
}
8998

9099
.ant-menu.cvat-job-item-menu {

cvat-ui/src/components/jobs-page/job-card.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useSelector } from 'react-redux';
88
import { useHistory } from 'react-router';
99
import Card from 'antd/lib/card';
1010
import Descriptions from 'antd/lib/descriptions';
11+
import Progress from 'antd/lib/progress';
1112
import { MoreOutlined } from '@ant-design/icons';
1213

1314
import { Job, JobType } from 'cvat-core-wrapper';
@@ -21,7 +22,7 @@ const useCardHeight = useCardHeightHOC({
2122
siblingClassNames: ['cvat-jobs-page-pagination', 'cvat-jobs-page-top-bar'],
2223
paddings: 80,
2324
minHeight: 200,
24-
numberOfRows: 3,
25+
numberOfRows: 2,
2526
});
2627

2728
interface Props {
@@ -88,7 +89,7 @@ function JobCardComponent(props: Readonly<Props>): JSX.Element {
8889
previewClassName='cvat-jobs-page-job-item-card-preview'
8990
/>
9091
<div className='cvat-job-page-list-item-id'>
91-
ID:
92+
ID:
9293
{` ${job.id}`}
9394
</div>
9495
{tag && <div className='cvat-job-page-list-item-type'>{tag}</div>}
@@ -101,14 +102,32 @@ function JobCardComponent(props: Readonly<Props>): JSX.Element {
101102
<Descriptions column={1} size='small'>
102103
<Descriptions.Item label='Task'>{job.taskName}</Descriptions.Item>
103104
<Descriptions.Item label='Stage and state'>{`${job.stage} ${job.state}`}</Descriptions.Item>
104-
<Descriptions.Item label='Frames'>{job.stopFrame - job.startFrame + 1}</Descriptions.Item>
105+
{/* Frames 항목은 progress bar 섹션으로 대체 - save 이력 있을 때 */}
106+
{job.annotationProgress == null && (
107+
<Descriptions.Item label='Frames'>{job.stopFrame - job.startFrame + 1}</Descriptions.Item>
108+
)}
105109
<Descriptions.Item label='Created'>{new Date(job.createdDate).toLocaleDateString()}</Descriptions.Item>
106110
{job.assignee ? (
107111
<Descriptions.Item label='Assignee'>{job.assignee.username}</Descriptions.Item>
108112
) : (
109113
<Descriptions.Item label='Assignee'> </Descriptions.Item>
110114
)}
111115
</Descriptions>
116+
{/* Save 이력이 있을 때만 progress bar 섹션 표시 */}
117+
{job.annotationProgress != null && (
118+
<div className='cvat-job-card-progress-section'>
119+
<div className='cvat-job-card-progress-info'>
120+
<span>{`Frame ${job.lastFrame} / ${job.stopFrame - job.startFrame + 1}`}</span>
121+
<span>{`${job.annotationProgress}%`}</span>
122+
</div>
123+
<Progress
124+
percent={job.annotationProgress}
125+
showInfo={false}
126+
strokeColor='#1890ff'
127+
size='small'
128+
/>
129+
</div>
130+
)}
112131
<div
113132
onClick={handleContextMenuClick}
114133
className='cvat-job-card-more-button cvat-actions-menu-button'

cvat-ui/src/components/jobs-page/styles.scss

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
.cvat-jobs-page-top-bar {
1212
margin-bottom: $grid-unit-size;
1313

14-
> div {
14+
>div {
1515
display: flex;
1616
justify-content: space-between;
1717

18-
> div {
18+
>div {
1919
display: flex;
2020
justify-content: space-between;
2121
align-items: center;
@@ -25,8 +25,8 @@
2525
width: $grid-unit-size * 32;
2626
}
2727

28-
> div {
29-
> *:not(:last-child) {
28+
>div {
29+
>*:not(:last-child) {
3030
margin-right: $grid-unit-size;
3131
}
3232

@@ -85,7 +85,7 @@
8585
height: 100%;
8686
width: 100%;
8787

88-
> .cvat-jobs-page-job-item-card-preview {
88+
>.cvat-jobs-page-job-item-card-preview {
8989
.ant-empty-image {
9090
height: $grid-unit-size * 10;
9191
}
@@ -166,16 +166,36 @@
166166
width: 100%;
167167
margin-bottom: $grid-unit-size;
168168

169-
> div:not(:first-child) {
169+
>div:not(:first-child) {
170170
padding-left: $grid-unit-size;
171171
}
172172
}
173173
}
174174

175+
// Save 이력이 있을 때 표시되는 progress bar 섹션
176+
// padding-right로 우하단 ··· 버튼과 겹치지 않도록 여백 확보
177+
.cvat-job-card-progress-section {
178+
margin-top: $grid-unit-size;
179+
padding-right: $grid-unit-size * 4;
180+
181+
.cvat-job-card-progress-info {
182+
display: flex;
183+
justify-content: space-between;
184+
font-size: 12px;
185+
color: rgba(0, 0, 0, 0.45);
186+
margin-bottom: 0;
187+
}
188+
189+
.ant-progress {
190+
line-height: 0;
191+
}
192+
}
193+
194+
// ··· 버튼 위치 (카드 우하단 고정)
175195
.cvat-job-card-more-button {
176196
position: absolute;
177197
bottom: $grid-unit-size * 2;
178198
right: $grid-unit-size;
179199
font-size: 16px;
180200
}
181-
}
201+
}

0 commit comments

Comments
 (0)