perf: 활동 피드 무제한 렌더링 방지(상한 + 페이지네이션)
활발한 프로젝트/업무에서 활동이 한 번에 전부 내려와 렌더링되던 문제 보완. - 업무 상세 활동 탭: 최신 100건으로 조회 상한 추가(기존 무제한) - 프로젝트 활동 피드: 200건 통째 조회 → 오프셋 페이지네이션 전환 - 컨트롤러 page/size 쿼리 지원, 서비스 findAndCount - 프론트 스토어 누적 + hasMore/loadMore, 활동 페이지 "더 보기" 버튼 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import {
|
||||
Controller,
|
||||
DefaultValuePipe,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
ParseUUIDPipe,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import type { PaginatedResult } from '../../common/pagination/pagination';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { ActivityService, type ActivityResponse } from './activity.service';
|
||||
|
||||
@@ -17,11 +21,13 @@ export class ActivityController {
|
||||
constructor(private readonly activityService: ActivityService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '프로젝트 활동 피드 (최신순)' })
|
||||
@ApiOperation({ summary: '프로젝트 활동 피드 (최신순, 페이지네이션)' })
|
||||
@ApiResponse({ status: 200, description: '조회 성공' })
|
||||
list(
|
||||
@Param('projectId', ParseUUIDPipe) projectId: string,
|
||||
): Promise<ActivityResponse[]> {
|
||||
return this.activityService.listForProject(projectId);
|
||||
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
||||
@Query('size', new DefaultValuePipe(20), ParseIntPipe) size: number,
|
||||
): Promise<PaginatedResult<ActivityResponse>> {
|
||||
return this.activityService.listForProject(projectId, page, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
paginated,
|
||||
resolvePage,
|
||||
type PaginatedResult,
|
||||
} from '../../common/pagination/pagination';
|
||||
import { UserService, type PublicUser } from '../user/user.service';
|
||||
import { Project } from '../project/entities/project.entity';
|
||||
import { Activity, type ActivityType } from './entities/activity.entity';
|
||||
|
||||
// 업무 상세 활동 탭 조회 상한 — 매우 활발한 업무에서 전량 렌더링을 방지(최신 N건)
|
||||
const TASK_ACTIVITY_LIMIT = 100;
|
||||
|
||||
// 활동 응답(원시) — 표시 문구/아이콘/톤은 프론트가 type+payload 로 조립
|
||||
export interface ActivityResponse {
|
||||
id: string;
|
||||
@@ -54,18 +62,29 @@ export class ActivityService {
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 활동 피드 — projectId(uuid)로 조회(최신순, 최대 200건). 날짜 그룹핑은 프론트.
|
||||
async listForProject(projectId: string): Promise<ActivityResponse[]> {
|
||||
const rows = await this.activityRepo.find({
|
||||
// 프로젝트 활동 피드 — projectId(uuid)로 조회(최신순, 오프셋 페이지네이션). 날짜 그룹핑은 프론트.
|
||||
async listForProject(
|
||||
projectId: string,
|
||||
page?: number,
|
||||
size?: number,
|
||||
): Promise<PaginatedResult<ActivityResponse>> {
|
||||
const pg = resolvePage(page, size);
|
||||
const [rows, total] = await this.activityRepo.findAndCount({
|
||||
where: { project: { id: projectId } },
|
||||
relations: ['actor'],
|
||||
order: { createdAt: 'DESC' },
|
||||
take: 200,
|
||||
skip: pg.skip,
|
||||
take: pg.take,
|
||||
});
|
||||
return rows.map((a) => this.toResponse(a));
|
||||
return paginated(
|
||||
rows.map((a) => this.toResponse(a)),
|
||||
total,
|
||||
pg,
|
||||
);
|
||||
}
|
||||
|
||||
// 업무 활동 — projectId(uuid) + 업무 순번(seq) 기준(최신순). 업무 상세 활동 탭용.
|
||||
// 전량 렌더링 방지를 위해 최신 TASK_ACTIVITY_LIMIT 건으로 제한한다.
|
||||
async listForTask(
|
||||
projectId: string,
|
||||
taskSeq: number,
|
||||
@@ -74,6 +93,7 @@ export class ActivityService {
|
||||
where: { project: { id: projectId }, taskSeq },
|
||||
relations: ['actor'],
|
||||
order: { createdAt: 'DESC' },
|
||||
take: TASK_ACTIVITY_LIMIT,
|
||||
});
|
||||
return rows.map((a) => this.toResponse(a));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user