refactor: 'Repo' 접두 타입·헬퍼 식별자를 'Project' 로 일괄 rename

저장소 기능 제거 후 남은 레거시 'Repo' 명칭을 프로젝트 의미에 맞게 정리(동작 변화 없음):
- 타입: ApiRepoMember→ApiProjectMember, ApiRepoTask→ApiProjectTask,
  ApiRepoTaskListResult→ApiProjectTaskListResult, RepoTask(View)→ProjectTask(View),
  RepoMember(Response)→ProjectMember(Response), RepoActivityItem/Day→ProjectActivityItem/Day,
  RepoTaskResponse/ListResult→ProjectTaskResponse/ListResult (백엔드 task.service 포함)
- 헬퍼: toRepoTaskView→toProjectTaskView, toRepoActivityItem→toProjectActivityItem,
  loadRepo→loadProject, filteredRepos→filteredProjects, removeRepo→removeProject
- 백엔드 spec mock repoRepo→projectRepo, 잔존 RepoDetailPage/저장소(Repo) 주석 정리

검증: 백엔드 build/lint 0·8 tests, 프론트 type-check/lint/build 0 (식별자 충돌 없음).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 01:52:23 +09:00
parent ef1757181d
commit be0a1bc9d1
22 changed files with 112 additions and 112 deletions
@@ -9,7 +9,7 @@ import {
} from 'typeorm';
import { ProjectMember } from './project-member.entity';
// 프로젝트 엔티티 — 작업의 기본 단위(우선). 저장소(Repo)는 프로젝트에 0~N개 연동된다.
// 프로젝트 엔티티 — 작업의 기본 단위. 업무·멤버·활동이 프로젝트에 연동된다.
@Entity('projects')
export class Project {
// 내부 식별자 (UUID) — 라우팅에도 사용
@@ -50,7 +50,7 @@ export class CreateTaskDto {
content?: string[];
@ApiProperty({
description: '담당자 사용자 ID 배열(저장소 멤버만 가능)',
description: '담당자 사용자 ID 배열(프로젝트 멤버만 가능)',
type: [String],
example: ['a1b2c3d4-...'],
})
@@ -55,7 +55,7 @@ export class UpdateTaskDto {
content?: string[];
@ApiProperty({
description: '담당자 사용자 ID 배열(저장소 멤버만 가능)',
description: '담당자 사용자 ID 배열(프로젝트 멤버만 가능)',
type: [String],
required: false,
})
@@ -13,7 +13,7 @@ import type { PublicUser } from '../user/user.service';
import { TaskService, type MyTaskRowResponse } from './task.service';
import type { PaginatedResult } from '../../common/pagination/pagination';
// 내 업무(저장소 횡단) — 담당/지시 집계. 그룹핑·라벨은 프론트가 파생
// 내 업무(프로젝트 횡단) — 담당/지시 집계. 그룹핑·라벨은 프론트가 파생
@ApiTags('MyTask')
@Controller('tasks')
@UseGuards(JwtAuthGuard)
@@ -21,7 +21,7 @@ export class MyTaskController {
constructor(private readonly taskService: TaskService) {}
@Get('assigned')
@ApiOperation({ summary: '내가 담당인 업무 (저장소 횡단, 페이지네이션)' })
@ApiOperation({ summary: '내가 담당인 업무 (프로젝트 횡단, 페이지네이션)' })
@ApiResponse({ status: 200, description: '조회 성공' })
listAssigned(
@CurrentUser() user: PublicUser,
@@ -32,7 +32,7 @@ export class MyTaskController {
}
@Get('issued')
@ApiOperation({ summary: '내가 지시한 업무 (저장소 횡단, 페이지네이션)' })
@ApiOperation({ summary: '내가 지시한 업무 (프로젝트 횡단, 페이지네이션)' })
@ApiResponse({ status: 200, description: '조회 성공' })
listIssued(
@CurrentUser() user: PublicUser,
+3 -3
View File
@@ -33,7 +33,7 @@ import {
type AttachmentResponse,
type CommentReplyResponse,
type CommentResponse,
type RepoTaskListResult,
type ProjectTaskListResult,
type TaskDetailResponse,
} from './task.service';
import { CreateTaskDto } from './dto/create-task.dto';
@@ -69,7 +69,7 @@ export class TaskController {
@Query('segment') segment?: string,
@Query('q') q?: string,
@Query('assignee') assignee?: string,
): Promise<RepoTaskListResult> {
): Promise<ProjectTaskListResult> {
return this.taskService.list(
projectId,
{ segment, q, assignee },
@@ -85,7 +85,7 @@ export class TaskController {
@ApiResponse({ status: 403, description: '권한 없음 (AUTH_003)' })
@ApiResponse({
status: 400,
description: '담당자가 저장소 멤버가 아님 (BIZ_001)',
description: '담당자가 프로젝트 멤버가 아님 (BIZ_001)',
})
create(
@Param('projectId') projectId: string,
+18 -18
View File
@@ -21,7 +21,7 @@ function makeService() {
createQueryBuilder: jest.fn(),
};
const checklistRepo = { create: jest.fn(), save: jest.fn() };
const repoRepo = { findOne: jest.fn() };
const projectRepo = { findOne: jest.fn() };
const memberRepo = { findOne: jest.fn(), find: jest.fn() };
// 상세 조립(buildDetail)에서 댓글/첨부는 빈 목록으로 모킹
const commentRepo = { find: jest.fn().mockResolvedValue([]) };
@@ -36,7 +36,7 @@ function makeService() {
const svc = new TaskService(
taskRepo as unknown as Repository<Task>,
checklistRepo as unknown as Repository<ChecklistItem>,
repoRepo as unknown as Repository<Project>,
projectRepo as unknown as Repository<Project>,
memberRepo as unknown as Repository<ProjectMember>,
commentRepo as unknown as Repository<Comment>,
attachmentRepo as unknown as Repository<Attachment>,
@@ -47,7 +47,7 @@ function makeService() {
svc,
taskRepo,
checklistRepo,
repoRepo,
projectRepo,
memberRepo,
commentRepo,
attachmentRepo,
@@ -60,8 +60,8 @@ const REPO = { id: 'repo-uuid', slugName: 'q3-launch', name: '캠페인' };
describe('TaskService', () => {
describe('changeStatus — 상태 머신', () => {
it('허용되지 않는 전이(todo→done)는 BusinessException', async () => {
const { svc, taskRepo, repoRepo, memberRepo } = makeService();
repoRepo.findOne.mockResolvedValue(REPO);
const { svc, taskRepo, projectRepo, memberRepo } = makeService();
projectRepo.findOne.mockResolvedValue(REPO);
memberRepo.findOne.mockResolvedValue({ roleType: 'admin' });
taskRepo.findOne.mockResolvedValue({
seq: 1,
@@ -77,8 +77,8 @@ describe('TaskService', () => {
});
it('승인(review→done)은 지시자/admin 이 아니면 Forbidden', async () => {
const { svc, taskRepo, repoRepo, memberRepo } = makeService();
repoRepo.findOne.mockResolvedValue(REPO);
const { svc, taskRepo, projectRepo, memberRepo } = makeService();
projectRepo.findOne.mockResolvedValue(REPO);
// 요청자는 일반 멤버(담당자)
memberRepo.findOne.mockResolvedValue({ roleType: 'member' });
taskRepo.findOne.mockResolvedValue({
@@ -95,8 +95,8 @@ describe('TaskService', () => {
});
it('비멤버는 상태 변경 불가(Forbidden)', async () => {
const { svc, repoRepo, memberRepo } = makeService();
repoRepo.findOne.mockResolvedValue(REPO);
const { svc, projectRepo, memberRepo } = makeService();
projectRepo.findOne.mockResolvedValue(REPO);
memberRepo.findOne.mockResolvedValue(null); // 멤버십 없음
await expect(
@@ -105,9 +105,9 @@ describe('TaskService', () => {
});
it('승인(review→done) 시 체크리스트를 모두 완료 처리한다', async () => {
const { svc, taskRepo, checklistRepo, repoRepo, memberRepo } =
const { svc, taskRepo, checklistRepo, projectRepo, memberRepo } =
makeService();
repoRepo.findOne.mockResolvedValue(REPO);
projectRepo.findOne.mockResolvedValue(REPO);
// 지시자/admin — assertMember 는 user 관계까지 로드(알림 actorName 용)
memberRepo.findOne.mockResolvedValue({
roleType: 'admin',
@@ -142,8 +142,8 @@ describe('TaskService', () => {
describe('승인 워크플로우 (5단계)', () => {
it('수정 요청(review→changes)은 지시자/admin 이 아니면 Forbidden', async () => {
const { svc, taskRepo, repoRepo, memberRepo } = makeService();
repoRepo.findOne.mockResolvedValue(REPO);
const { svc, taskRepo, projectRepo, memberRepo } = makeService();
projectRepo.findOne.mockResolvedValue(REPO);
memberRepo.findOne.mockResolvedValue({
roleType: 'member',
user: { id: 'assignee' },
@@ -165,12 +165,12 @@ describe('TaskService', () => {
const {
svc,
taskRepo,
repoRepo,
projectRepo,
memberRepo,
commentRepo,
attachmentRepo,
} = makeService();
repoRepo.findOne.mockResolvedValue(REPO);
projectRepo.findOne.mockResolvedValue(REPO);
memberRepo.findOne.mockResolvedValue({
roleType: 'member',
user: { id: 'assignee', email: 'a@b.c', name: 'A', role: null },
@@ -212,9 +212,9 @@ describe('TaskService', () => {
});
describe('create — 담당자 멤버 검증', () => {
it('담당자가 저장소 멤버가 아니면 BusinessException', async () => {
const { svc, repoRepo, memberRepo } = makeService();
repoRepo.findOne.mockResolvedValue(REPO);
it('담당자가 프로젝트 멤버가 아니면 BusinessException', async () => {
const { svc, projectRepo, memberRepo } = makeService();
projectRepo.findOne.mockResolvedValue(REPO);
// assertAdmin 통과
memberRepo.findOne.mockResolvedValue({
roleType: 'admin',
+20 -20
View File
@@ -36,8 +36,8 @@ import { join } from 'path';
import { unlink } from 'fs/promises';
// 목록 행 응답 — 라벨류는 프론트가 파생(dueLabel/dday/statusLabel 등)
export interface RepoTaskResponse {
id: number; // 저장소 내 순번(seq)
export interface ProjectTaskResponse {
id: number; // 프로젝트 내 순번(seq)
title: string;
status: TaskStatus;
dueDate: string | null;
@@ -54,7 +54,7 @@ export interface ChecklistItemResponse {
done: boolean;
}
// 저장소 업무 목록의 세그먼트별 카운트(전체/진행/대기/완료) — 검색어 반영
// 프로젝트 업무 목록의 세그먼트별 카운트(전체/진행/대기/완료) — 검색어 반영
export interface TaskSegmentCounts {
all: number;
prog: number; // 진행 중(prog+review)
@@ -62,8 +62,8 @@ export interface TaskSegmentCounts {
done: number;
}
// 저장소 업무 목록 응답 — 페이지 + 세그먼트 카운트
export type RepoTaskListResult = PaginatedResult<RepoTaskResponse> & {
// 프로젝트 업무 목록 응답 — 페이지 + 세그먼트 카운트
export type ProjectTaskListResult = PaginatedResult<ProjectTaskResponse> & {
counts: TaskSegmentCounts;
};
@@ -96,9 +96,9 @@ export interface CommentResponse {
replies: CommentReplyResponse[];
}
// 내 업무 행(저장소 횡단) — 라벨/그룹핑은 프론트가 파생
// 내 업무 행(프로젝트 횡단) — 라벨/그룹핑은 프론트가 파생
export interface MyTaskRowResponse {
id: number; // 저장소 내 순번(seq)
id: number; // 프로젝트 내 순번(seq)
projectId: string; // projectId (라우팅용)
projectName: string;
title: string;
@@ -195,7 +195,7 @@ export class TaskService {
filters: { segment?: string; q?: string; assignee?: string },
page?: number,
size?: number,
): Promise<RepoTaskListResult> {
): Promise<ProjectTaskListResult> {
const project = await this.getProjectOrThrow(projectId);
const pg = resolvePage(page, size);
@@ -262,7 +262,7 @@ export class TaskService {
return { ...paginated(items, total, pg), counts };
}
// 세그먼트별 카운트(검색어 반영) — RepoDetailPage 세그먼트 배지용
// 세그먼트별 카운트(검색어 반영) — ProjectDetailPage 세그먼트 배지용
private async segmentCounts(
projectId: string,
q?: string,
@@ -286,7 +286,7 @@ export class TaskService {
if (r.status === 'prog' || r.status === 'review') counts.prog += c;
else if (r.status === 'todo') counts.todo += c;
else if (r.status === 'done') counts.done += c;
// 'changes'(수정 요청)는 전체에만 집계(기존 RepoDetailPage 세그먼트 동작과 동일)
// 'changes'(수정 요청)는 전체에만 집계(기존 ProjectDetailPage 세그먼트 동작과 동일)
}
return counts;
}
@@ -298,7 +298,7 @@ export class TaskService {
return this.buildDetail(task, project);
}
// 업무 생성 — admin(지시자) 권한. 담당자는 저장소 멤버만 지정 가능
// 업무 생성 — admin(지시자) 권한. 담당자는 프로젝트 멤버만 지정 가능
async create(
projectId: string,
actingUserId: string,
@@ -628,7 +628,7 @@ export class TaskService {
// --- 5단계: 댓글 / 답글 ---
// 댓글 작성 — 저장소 멤버. fileId 로 기존 첨부 연결 가능
// 댓글 작성 — 프로젝트 멤버. fileId 로 기존 첨부 연결 가능
async addComment(
projectId: string,
actingUserId: string,
@@ -683,7 +683,7 @@ export class TaskService {
);
}
// 답글 작성 — 저장소 멤버. 최상위 댓글에만 답글 가능(1단계 깊이)
// 답글 작성 — 프로젝트 멤버. 최상위 댓글에만 답글 가능(1단계 깊이)
async addReply(
projectId: string,
actingUserId: string,
@@ -745,7 +745,7 @@ export class TaskService {
// --- 5단계: 파일 첨부 ---
// 파일 업로드 — 저장소 멤버. 디스크 저장은 multer 가 처리, 메타데이터만 영속
// 파일 업로드 — 프로젝트 멤버. 디스크 저장은 multer 가 처리, 메타데이터만 영속
async addFile(
projectId: string,
actingUserId: string,
@@ -784,7 +784,7 @@ export class TaskService {
);
}
// 파일 삭제 — 저장소 멤버. 디스크 파일도 정리
// 파일 삭제 — 프로젝트 멤버. 디스크 파일도 정리
async removeFile(
projectId: string,
actingUserId: string,
@@ -897,7 +897,7 @@ export class TaskService {
);
}
// --- 7단계: 내 업무(저장소 횡단 집계) ---
// --- 7단계: 내 업무(프로젝트 횡단 집계) ---
// 내가 담당인 업무 — 마감 임박순(마감 없음은 뒤로), 오프셋 페이지네이션
async listAssigned(
@@ -1120,7 +1120,7 @@ export class TaskService {
return membership;
}
// 저장소 멤버 확인 — 멤버십(+user) 반환. 아니면 403
// 프로젝트 멤버 확인 — 멤버십(+user) 반환. 아니면 403
private async assertMember(
projectId: string,
userId: string,
@@ -1135,7 +1135,7 @@ export class TaskService {
return membership;
}
// 담당자 ID 목록 → User[] 로 변환하며 "저장소 멤버" 인지 검증
// 담당자 ID 목록 → User[] 로 변환하며 "프로젝트 멤버" 인지 검증
private async resolveMemberAssignees(
projectId: string,
assigneeIds: string[],
@@ -1148,7 +1148,7 @@ export class TaskService {
if (memberships.length !== uniqueIds.length) {
throw new BusinessException(
'BIZ_001',
'담당자는 저장소 멤버만 지정할 수 있습니다.',
'담당자는 프로젝트 멤버만 지정할 수 있습니다.',
HttpStatus.BAD_REQUEST,
);
}
@@ -1203,7 +1203,7 @@ export class TaskService {
task: Task,
commentCount: number,
fileCount: number,
): RepoTaskResponse {
): ProjectTaskResponse {
const checklist = task.checklist ?? [];
const done = checklist.filter((c) => c.done).length;
return {