From be0a1bc9d1b15f9e100f71b221215b2e553c16ae Mon Sep 17 00:00:00 2001 From: ttipo Date: Sun, 21 Jun 2026 01:52:23 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20'Repo'=20=EC=A0=91=EB=91=90=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=C2=B7=ED=97=AC=ED=8D=BC=20=EC=8B=9D=EB=B3=84?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20'Project'=20=EB=A1=9C=20=EC=9D=BC=EA=B4=84?= =?UTF-8?q?=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 저장소 기능 제거 후 남은 레거시 '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) --- .../project/entities/project.entity.ts | 2 +- .../src/modules/task/dto/create-task.dto.ts | 2 +- .../src/modules/task/dto/update-task.dto.ts | 2 +- .../src/modules/task/my-task.controller.ts | 6 +-- backend/src/modules/task/task.controller.ts | 6 +-- backend/src/modules/task/task.service.spec.ts | 36 ++++++++--------- backend/src/modules/task/task.service.ts | 40 +++++++++---------- frontend/src/composables/useMember.ts | 14 +++---- frontend/src/composables/useMyTask.ts | 2 +- frontend/src/composables/useTask.ts | 6 +-- frontend/src/mock/relay.mock.ts | 14 +++---- .../src/pages/relay/ProjectDetailPage.vue | 4 +- frontend/src/pages/relay/ProjectListPage.vue | 6 +-- .../src/pages/relay/ProjectMembersPage.vue | 4 +- .../src/pages/relay/ProjectSettingsPage.vue | 8 ++-- frontend/src/pages/relay/TaskCreatePage.vue | 10 ++--- frontend/src/stores/activity.store.ts | 16 ++++---- frontend/src/stores/member.store.ts | 12 +++--- frontend/src/stores/task.store.ts | 16 ++++---- frontend/src/types/member.ts | 6 +-- frontend/src/types/my-task.ts | 2 +- frontend/src/types/task.ts | 10 ++--- 22 files changed, 112 insertions(+), 112 deletions(-) diff --git a/backend/src/modules/project/entities/project.entity.ts b/backend/src/modules/project/entities/project.entity.ts index 68f7969..46dd0e7 100644 --- a/backend/src/modules/project/entities/project.entity.ts +++ b/backend/src/modules/project/entities/project.entity.ts @@ -9,7 +9,7 @@ import { } from 'typeorm'; import { ProjectMember } from './project-member.entity'; -// 프로젝트 엔티티 — 작업의 기본 단위(우선). 저장소(Repo)는 프로젝트에 0~N개 연동된다. +// 프로젝트 엔티티 — 작업의 기본 단위. 업무·멤버·활동이 프로젝트에 연동된다. @Entity('projects') export class Project { // 내부 식별자 (UUID) — 라우팅에도 사용 diff --git a/backend/src/modules/task/dto/create-task.dto.ts b/backend/src/modules/task/dto/create-task.dto.ts index 3462e25..a988972 100644 --- a/backend/src/modules/task/dto/create-task.dto.ts +++ b/backend/src/modules/task/dto/create-task.dto.ts @@ -50,7 +50,7 @@ export class CreateTaskDto { content?: string[]; @ApiProperty({ - description: '담당자 사용자 ID 배열(저장소 멤버만 가능)', + description: '담당자 사용자 ID 배열(프로젝트 멤버만 가능)', type: [String], example: ['a1b2c3d4-...'], }) diff --git a/backend/src/modules/task/dto/update-task.dto.ts b/backend/src/modules/task/dto/update-task.dto.ts index f3299ba..a6cfced 100644 --- a/backend/src/modules/task/dto/update-task.dto.ts +++ b/backend/src/modules/task/dto/update-task.dto.ts @@ -55,7 +55,7 @@ export class UpdateTaskDto { content?: string[]; @ApiProperty({ - description: '담당자 사용자 ID 배열(저장소 멤버만 가능)', + description: '담당자 사용자 ID 배열(프로젝트 멤버만 가능)', type: [String], required: false, }) diff --git a/backend/src/modules/task/my-task.controller.ts b/backend/src/modules/task/my-task.controller.ts index 7cb24ea..0f704f3 100644 --- a/backend/src/modules/task/my-task.controller.ts +++ b/backend/src/modules/task/my-task.controller.ts @@ -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, diff --git a/backend/src/modules/task/task.controller.ts b/backend/src/modules/task/task.controller.ts index 6f47875..6e9397f 100644 --- a/backend/src/modules/task/task.controller.ts +++ b/backend/src/modules/task/task.controller.ts @@ -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 { + ): Promise { 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, diff --git a/backend/src/modules/task/task.service.spec.ts b/backend/src/modules/task/task.service.spec.ts index 9a0384e..1947494 100644 --- a/backend/src/modules/task/task.service.spec.ts +++ b/backend/src/modules/task/task.service.spec.ts @@ -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, checklistRepo as unknown as Repository, - repoRepo as unknown as Repository, + projectRepo as unknown as Repository, memberRepo as unknown as Repository, commentRepo as unknown as Repository, attachmentRepo as unknown as Repository, @@ -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', diff --git a/backend/src/modules/task/task.service.ts b/backend/src/modules/task/task.service.ts index 3461d3a..08aeace 100644 --- a/backend/src/modules/task/task.service.ts +++ b/backend/src/modules/task/task.service.ts @@ -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 & { +// 프로젝트 업무 목록 응답 — 페이지 + 세그먼트 카운트 +export type ProjectTaskListResult = PaginatedResult & { 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 { + ): Promise { 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 { diff --git a/frontend/src/composables/useMember.ts b/frontend/src/composables/useMember.ts index 18aa556..80268e0 100644 --- a/frontend/src/composables/useMember.ts +++ b/frontend/src/composables/useMember.ts @@ -1,6 +1,6 @@ import { useApi } from './useApi' import type { - ApiRepoMember, + ApiProjectMember, InviteMemberPayload, UpdateMemberPayload, } from '@/types/member' @@ -16,21 +16,21 @@ export function useMember() { projectId: string, page = 1, size = 20, - ): Promise> { + ): Promise> { return (await api.get(`/projects/${projectId}/members`, { params: { page, size }, - })) as unknown as Paginated + })) as unknown as Paginated } // 멤버 초대 (admin) — 가입된 사용자만 이메일로 초대 async function invite( projectId: string, payload: InviteMemberPayload, - ): Promise { + ): Promise { return (await api.post( `/projects/${projectId}/members`, payload, - )) as unknown as ApiRepoMember + )) as unknown as ApiProjectMember } // 멤버 역할/직무 변경 (admin, owner 제외) @@ -38,11 +38,11 @@ export function useMember() { projectId: string, userId: string, payload: UpdateMemberPayload, - ): Promise { + ): Promise { return (await api.patch( `/projects/${projectId}/members/${userId}`, payload, - )) as unknown as ApiRepoMember + )) as unknown as ApiProjectMember } // 멤버 제거 (admin, owner 제외) diff --git a/frontend/src/composables/useMyTask.ts b/frontend/src/composables/useMyTask.ts index 3ff9033..36ccf9b 100644 --- a/frontend/src/composables/useMyTask.ts +++ b/frontend/src/composables/useMyTask.ts @@ -2,7 +2,7 @@ import { useApi } from './useApi' import type { ApiMyTaskRow } from '@/types/my-task' import type { Paginated } from '@/types/pagination' -// 내 업무 API Composable — 저장소 횡단 집계(담당/지시), 오프셋 페이지네이션 +// 내 업무 API Composable — 프로젝트 횡단 집계(담당/지시), 오프셋 페이지네이션 export function useMyTask() { const api = useApi() diff --git a/frontend/src/composables/useTask.ts b/frontend/src/composables/useTask.ts index 24cd0ad..9bafdcb 100644 --- a/frontend/src/composables/useTask.ts +++ b/frontend/src/composables/useTask.ts @@ -3,7 +3,7 @@ import type { ApiAttachment, ApiComment, ApiCommentReply, - ApiRepoTaskListResult, + ApiProjectTaskListResult, ApiTaskDetail, CreateTaskPayload, TaskListQuery, @@ -23,10 +23,10 @@ export function useTask() { query: TaskListQuery = {}, page = 1, size = DEFAULT_PAGE_SIZE, - ): Promise { + ): Promise { return (await api.get(`/projects/${projectId}/tasks`, { params: { ...query, page, size }, - })) as unknown as ApiRepoTaskListResult + })) as unknown as ApiProjectTaskListResult } // 업무 단건 상세 (taskId = 프로젝트 내 순번) diff --git a/frontend/src/mock/relay.mock.ts b/frontend/src/mock/relay.mock.ts index 7e2b043..3aad2c7 100644 --- a/frontend/src/mock/relay.mock.ts +++ b/frontend/src/mock/relay.mock.ts @@ -56,8 +56,8 @@ export interface Project { isOwner?: boolean } -/** 저장소 상세의 업무 목록 행 */ -export interface RepoTask { +/** 프로젝트의 업무 목록 행 */ +export interface ProjectTask { id: number title: string status: TaskStatus @@ -187,8 +187,8 @@ export function countActive(groups: MyTaskGroup[]): number { return groups.filter((g) => g.key !== 'done').reduce((sum, g) => sum + g.rows.length, 0) } -/** 저장소 멤버 */ -export interface RepoMember { +/** 프로젝트 멤버 */ +export interface ProjectMember { user: User email: string /** 담당 업무 수 */ @@ -212,7 +212,7 @@ export type ActivityIcon = | 'repo' /** 활동 항목 */ -export interface RepoActivityItem { +export interface ProjectActivityItem { /** 아이콘 색상 톤 */ tone: 'task' | 'member' | 'setting' /** 아이콘 모양 */ @@ -225,7 +225,7 @@ export interface RepoActivityItem { } /** 날짜 그룹 */ -export interface RepoActivityDay { +export interface ProjectActivityDay { day: string - items: RepoActivityItem[] + items: ProjectActivityItem[] } diff --git a/frontend/src/pages/relay/ProjectDetailPage.vue b/frontend/src/pages/relay/ProjectDetailPage.vue index d514a74..73096a5 100644 --- a/frontend/src/pages/relay/ProjectDetailPage.vue +++ b/frontend/src/pages/relay/ProjectDetailPage.vue @@ -21,7 +21,7 @@ const { tasks, counts, hasMore, loadingMore, loading } = storeToRefs(taskStore) // 저장소 헤더/진행률 — API 연동 const repo = ref(null) -async function loadRepo() { +async function loadProject() { try { repo.value = await projectStore.fetchOne(projectId.value) } catch { @@ -60,7 +60,7 @@ async function loadMoreTasks() { watch( projectId, () => { - loadRepo() + loadProject() loadTasks() }, { immediate: true }, diff --git a/frontend/src/pages/relay/ProjectListPage.vue b/frontend/src/pages/relay/ProjectListPage.vue index e95e2e2..0a8f084 100644 --- a/frontend/src/pages/relay/ProjectListPage.vue +++ b/frontend/src/pages/relay/ProjectListPage.vue @@ -17,7 +17,7 @@ onMounted(() => { }) // 검색어 필터 적용 -const filteredRepos = computed(() => +const filteredProjects = computed(() => projects.value.filter( (project) => !keyword.value || project.name.includes(keyword.value), ), @@ -97,7 +97,7 @@ const filteredRepos = computed(() =>
불러오는 중…
{{ projects.length === 0 ? '아직 프로젝트가 없습니다. 새 프로젝트를 만들어 보세요.' : '검색 결과가 없습니다.' }} diff --git a/frontend/src/pages/relay/ProjectMembersPage.vue b/frontend/src/pages/relay/ProjectMembersPage.vue index fcb7803..dd13c9d 100644 --- a/frontend/src/pages/relay/ProjectMembersPage.vue +++ b/frontend/src/pages/relay/ProjectMembersPage.vue @@ -22,7 +22,7 @@ const memberStore = useMemberStore() // 프로젝트 헤더 — API 연동 const repo = ref(null) -async function loadRepo() { +async function loadProject() { try { repo.value = await projectStore.fetchOne(projectId.value) } catch { @@ -44,7 +44,7 @@ async function loadMembers() { watch( projectId, () => { - loadRepo() + loadProject() loadMembers() }, { immediate: true }, diff --git a/frontend/src/pages/relay/ProjectSettingsPage.vue b/frontend/src/pages/relay/ProjectSettingsPage.vue index 76b80bb..31d42ff 100644 --- a/frontend/src/pages/relay/ProjectSettingsPage.vue +++ b/frontend/src/pages/relay/ProjectSettingsPage.vue @@ -29,7 +29,7 @@ const description = ref('') const saving = ref(false) // 프로젝트 로드 + 폼 초기화 -async function loadRepo() { +async function loadProject() { try { repo.value = await projectStore.fetchOne(projectId.value) } catch { @@ -45,7 +45,7 @@ async function loadRepo() { displayTitle.value = repo.value.name description.value = repo.value.desc } -watch(projectId, loadRepo, { immediate: true }) +watch(projectId, loadProject, { immediate: true }) // 변경 저장 (admin 전용) async function save() { @@ -65,7 +65,7 @@ async function save() { } // 프로젝트 삭제 (소유자 전용) -async function removeRepo() { +async function removeProject() { if (!repo.value || !isOwner.value) return if (!window.confirm('프로젝트와 그 안의 모든 업무·활동 기록이 영구 삭제됩니다. 계속할까요?')) return try { @@ -200,7 +200,7 @@ async function removeRepo() { class="btn-danger solid" type="button" :disabled="!isOwner" - @click="removeRepo" + @click="removeProject" > 프로젝트 삭제 diff --git a/frontend/src/pages/relay/TaskCreatePage.vue b/frontend/src/pages/relay/TaskCreatePage.vue index 019a1f2..67c7103 100644 --- a/frontend/src/pages/relay/TaskCreatePage.vue +++ b/frontend/src/pages/relay/TaskCreatePage.vue @@ -53,14 +53,14 @@ function toDateInput(iso: string): string { // 저장소 헤더(빵부스러기) — API 연동 const repo = ref(null) -async function loadRepo() { +async function loadProject() { try { repo.value = await projectStore.fetchOne(projectId.value) } catch { repo.value = null } } -// 담당자 후보 = 저장소 멤버 +// 담당자 후보 = 프로젝트 멤버 async function loadMembers() { try { await memberStore.fetchList(projectId.value) @@ -87,7 +87,7 @@ async function loadTaskForEdit() { watch( [projectId, taskId], () => { - loadRepo() + loadProject() loadMembers() loadTaskForEdit() }, @@ -109,7 +109,7 @@ const issuer = computed(() => { } }) -// --- 담당자 선택 (저장소 멤버 중) --- +// --- 담당자 선택 (프로젝트 멤버 중) --- const assignees = ref([]) const assigneeQuery = ref('') const showAssigneeDropdown = ref(false) @@ -692,7 +692,7 @@ function goBack() { v-if="assigneeCandidates.length === 0" class="assignee-dd-msg" > - {{ memberStore.members.length === 0 ? '저장소 멤버가 없습니다.' : '추가할 멤버가 없습니다.' }} + {{ memberStore.members.length === 0 ? '프로젝트 멤버가 없습니다.' : '추가할 멤버가 없습니다.' }}
diff --git a/frontend/src/stores/activity.store.ts b/frontend/src/stores/activity.store.ts index 9e43c1f..05207c1 100644 --- a/frontend/src/stores/activity.store.ts +++ b/frontend/src/stores/activity.store.ts @@ -8,7 +8,7 @@ import { relativeTimeKo, } from '@/shared/utils/format' import type { ApiActivity } from '@/types/activity' -import type { RepoActivityDay, RepoActivityItem } from '@/mock/relay.mock' +import type { ProjectActivityDay, ProjectActivityItem } from '@/mock/relay.mock' // payload(자유형)에서 문자열 값 안전 추출 function pstr(payload: Record, key: string): string { @@ -19,16 +19,16 @@ function pstr(payload: Record, key: string): string { // 행위자/시간 등 공통 필드 function baseOf( api: ApiActivity, -): Pick { +): Pick { return { avatarUrl: api.actor?.avatarUrl ?? null, time: relativeTimeKo(api.createdAt), } } -// API 활동 → 화면용 RepoActivityItem (type+payload 로 톤/아이콘/문구 조립) +// API 활동 → 화면용 ProjectActivityItem (type+payload 로 톤/아이콘/문구 조립) // 사용자 입력(이름/제목/파일명/표시명)은 escapeHtml 로 감싸 XSS 를 방지한다. -function toRepoActivityItem(api: ApiActivity): RepoActivityItem { +function toProjectActivityItem(api: ApiActivity): ProjectActivityItem { const base = baseOf(api) const actor = escapeHtml(api.actor?.name ?? '알 수 없음') const p = api.payload @@ -114,7 +114,7 @@ function toRepoActivityItem(api: ApiActivity): RepoActivityItem { // 활동 스토어 — 저장소 활동 피드(날짜 그룹) export const useActivityStore = defineStore('activity', () => { - const days = ref([]) + const days = ref([]) const loading = ref(false) const activityApi = useActivity() @@ -124,15 +124,15 @@ export const useActivityStore = defineStore('activity', () => { loading.value = true try { const list = await activityApi.list(projectId) - const groups: RepoActivityDay[] = [] - let current: RepoActivityDay | null = null + const groups: ProjectActivityDay[] = [] + let current: ProjectActivityDay | null = null for (const a of list) { const day = dayGroupKo(a.createdAt) if (!current || current.day !== day) { current = { day, items: [] } groups.push(current) } - current.items.push(toRepoActivityItem(a)) + current.items.push(toProjectActivityItem(a)) } days.value = groups } finally { diff --git a/frontend/src/stores/member.store.ts b/frontend/src/stores/member.store.ts index 39d41f8..3387fbe 100644 --- a/frontend/src/stores/member.store.ts +++ b/frontend/src/stores/member.store.ts @@ -4,14 +4,14 @@ import { useMember } from '@/composables/useMember' import { useAuthStore } from '@/stores/auth.store' import { DEFAULT_PAGE_SIZE } from '@/types/pagination' import type { - ApiRepoMember, + ApiProjectMember, InviteMemberPayload, UpdateMemberPayload, } from '@/types/member' -import type { RepoMember as MemberView } from '@/mock/relay.mock' +import type { ProjectMember as MemberView } from '@/mock/relay.mock' -// API 멤버 → 화면용 RepoMember(이니셜/색상 파생 + isYou 산출) -function toMemberView(api: ApiRepoMember, currentUserId: string | null): MemberView { +// API 멤버 → 화면용 ProjectMember(이니셜/색상 파생 + isYou 산출) +function toMemberView(api: ApiProjectMember, currentUserId: string | null): MemberView { return { user: { id: api.user.id, @@ -27,7 +27,7 @@ function toMemberView(api: ApiRepoMember, currentUserId: string | null): MemberV } } -// 저장소 멤버 스토어 — 현재 저장소의 멤버 목록 상태 + 초대/역할변경/제거 액션 +// 프로젝트 멤버 스토어 — 현재 저장소의 멤버 목록 상태 + 초대/역할변경/제거 액션 export const useMemberStore = defineStore('member', () => { const members = ref([]) const loading = ref(false) @@ -46,7 +46,7 @@ export const useMemberStore = defineStore('member', () => { async function fetchList(projectId: string): Promise { loading.value = true try { - const acc: ApiRepoMember[] = [] + const acc: ApiProjectMember[] = [] let page = 1 for (;;) { const { items, total } = await memberApi.list(projectId, page, DEFAULT_PAGE_SIZE) diff --git a/frontend/src/stores/task.store.ts b/frontend/src/stores/task.store.ts index 06c9939..f167019 100644 --- a/frontend/src/stores/task.store.ts +++ b/frontend/src/stores/task.store.ts @@ -17,7 +17,7 @@ import type { import type { ApiAttachment, ApiComment, - ApiRepoTask, + ApiProjectTask, ApiTaskDetail, CreateTaskPayload, TaskListQuery, @@ -29,7 +29,7 @@ import type { Activity as ActivityView, Attachment as AttachmentView, Comment as CommentView, - RepoTask as RepoTaskView, + ProjectTask as ProjectTaskView, TaskDetail as TaskDetailView, TaskStatus, User as UserView, @@ -52,8 +52,8 @@ const UNKNOWN_USER: UserView = { role: '', } -// API 업무 목록 행 → 화면용 RepoTask -function toRepoTaskView(api: ApiRepoTask): RepoTaskView { +// API 업무 목록 행 → 화면용 ProjectTask +function toProjectTaskView(api: ApiProjectTask): ProjectTaskView { const due = taskDueInfo(api.dueDate, api.status === 'done') const [, total] = api.checklist return { @@ -198,12 +198,12 @@ function toTaskDetailView(api: ApiTaskDetail): TaskDetailView { } } -// 업무 스토어 — 저장소 업무 목록 + 현재 상세 + CRUD/상태 액션 +// 업무 스토어 — 프로젝트 업무 목록 + 현재 상세 + CRUD/상태 액션 // 빈 세그먼트 카운트(초기값) const EMPTY_COUNTS: TaskSegmentCounts = { all: 0, prog: 0, todo: 0, done: 0 } export const useTaskStore = defineStore('task', () => { - const tasks = ref([]) + const tasks = ref([]) const current = ref(null) const loading = ref(false) // 목록 페이지네이션(더보기) + 세그먼트 카운트 상태 @@ -220,7 +220,7 @@ export const useTaskStore = defineStore('task', () => { loading.value = true try { const res = await taskApi.list(projectId, query, 1, DEFAULT_PAGE_SIZE) - tasks.value = res.items.map(toRepoTaskView) + tasks.value = res.items.map(toProjectTaskView) total.value = res.total counts.value = res.counts page.value = 1 @@ -235,7 +235,7 @@ export const useTaskStore = defineStore('task', () => { loadingMore.value = true try { const res = await taskApi.list(projectId, query, page.value + 1, DEFAULT_PAGE_SIZE) - tasks.value.push(...res.items.map(toRepoTaskView)) + tasks.value.push(...res.items.map(toProjectTaskView)) total.value = res.total counts.value = res.counts page.value += 1 diff --git a/frontend/src/types/member.ts b/frontend/src/types/member.ts index 8ccf3c2..faedf28 100644 --- a/frontend/src/types/member.ts +++ b/frontend/src/types/member.ts @@ -1,12 +1,12 @@ -// 저장소 멤버 도메인 타입 — 백엔드 RepoMemberResponse / DTO 와 1:1 +// 프로젝트 멤버 도메인 타입 — 백엔드 ProjectMemberResponse / DTO 와 1:1 import type { ApiMember } from '@/types/repo' // 권한 역할 export type MemberRoleType = 'admin' | 'member' -// 멤버(API 원시) — 백엔드 RepoMemberResponse 와 1:1 (isYou 는 프론트가 산출) -export interface ApiRepoMember { +// 멤버(API 원시) — 백엔드 ProjectMemberResponse 와 1:1 (isYou 는 프론트가 산출) +export interface ApiProjectMember { user: ApiMember // { id, email, name, role } email: string taskCount: number diff --git a/frontend/src/types/my-task.ts b/frontend/src/types/my-task.ts index 546cb05..253afe6 100644 --- a/frontend/src/types/my-task.ts +++ b/frontend/src/types/my-task.ts @@ -1,4 +1,4 @@ -// 내 업무(저장소 횡단) 도메인 타입 — 백엔드 MyTaskRowResponse 와 1:1 +// 내 업무(프로젝트 횡단) 도메인 타입 — 백엔드 MyTaskRowResponse 와 1:1 import type { TaskStatus } from '@/mock/relay.mock' import type { ApiMember } from '@/types/repo' diff --git a/frontend/src/types/task.ts b/frontend/src/types/task.ts index d0012e7..071fa09 100644 --- a/frontend/src/types/task.ts +++ b/frontend/src/types/task.ts @@ -5,8 +5,8 @@ import type { ApiMember } from '@/types/repo' import type { ApiActivity } from '@/types/activity' import type { Paginated } from '@/types/pagination' -// 업무 목록 행(API 원시) — 백엔드 RepoTaskResponse 와 1:1 -export interface ApiRepoTask { +// 업무 목록 행(API 원시) — 백엔드 ProjectTaskResponse 와 1:1 +export interface ApiProjectTask { id: number // 프로젝트 내 순번(seq) title: string status: TaskStatus @@ -83,7 +83,7 @@ export interface ApiTaskDetail { // 업무 목록 세그먼트(상태 묶음) — 진행 중 = prog+review export type TaskSegment = 'all' | 'prog' | 'todo' | 'done' -// 세그먼트별 카운트(검색어 반영) — RepoDetailPage 세그먼트 배지용. 백엔드 TaskSegmentCounts 와 1:1 +// 세그먼트별 카운트(검색어 반영) — ProjectDetailPage 세그먼트 배지용. 백엔드 TaskSegmentCounts 와 1:1 export interface TaskSegmentCounts { all: number prog: number @@ -98,8 +98,8 @@ export interface TaskListQuery { assignee?: string } -// 업무 목록 응답 — 페이지네이션 + 세그먼트 카운트 동봉 (백엔드 RepoTaskListResult 와 1:1) -export type ApiRepoTaskListResult = Paginated & { +// 업무 목록 응답 — 페이지네이션 + 세그먼트 카운트 동봉 (백엔드 ProjectTaskListResult 와 1:1) +export type ApiProjectTaskListResult = Paginated & { counts: TaskSegmentCounts }