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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Paginated<ApiRepoMember>> {
|
||||
): Promise<Paginated<ApiProjectMember>> {
|
||||
return (await api.get(`/projects/${projectId}/members`, {
|
||||
params: { page, size },
|
||||
})) as unknown as Paginated<ApiRepoMember>
|
||||
})) as unknown as Paginated<ApiProjectMember>
|
||||
}
|
||||
|
||||
// 멤버 초대 (admin) — 가입된 사용자만 이메일로 초대
|
||||
async function invite(
|
||||
projectId: string,
|
||||
payload: InviteMemberPayload,
|
||||
): Promise<ApiRepoMember> {
|
||||
): Promise<ApiProjectMember> {
|
||||
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<ApiRepoMember> {
|
||||
): Promise<ApiProjectMember> {
|
||||
return (await api.patch(
|
||||
`/projects/${projectId}/members/${userId}`,
|
||||
payload,
|
||||
)) as unknown as ApiRepoMember
|
||||
)) as unknown as ApiProjectMember
|
||||
}
|
||||
|
||||
// 멤버 제거 (admin, owner 제외)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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<ApiRepoTaskListResult> {
|
||||
): Promise<ApiProjectTaskListResult> {
|
||||
return (await api.get(`/projects/${projectId}/tasks`, {
|
||||
params: { ...query, page, size },
|
||||
})) as unknown as ApiRepoTaskListResult
|
||||
})) as unknown as ApiProjectTaskListResult
|
||||
}
|
||||
|
||||
// 업무 단건 상세 (taskId = 프로젝트 내 순번)
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const { tasks, counts, hasMore, loadingMore, loading } = storeToRefs(taskStore)
|
||||
|
||||
// 저장소 헤더/진행률 — API 연동
|
||||
const repo = ref<Project | null>(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 },
|
||||
|
||||
@@ -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(() =>
|
||||
<!-- 목록 -->
|
||||
<div class="list">
|
||||
<RouterLink
|
||||
v-for="repo in filteredRepos"
|
||||
v-for="repo in filteredProjects"
|
||||
:key="repo.id"
|
||||
class="repo-row"
|
||||
:to="`/projects/${repo.id}`"
|
||||
@@ -176,7 +176,7 @@ const filteredRepos = computed(() =>
|
||||
불러오는 중…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredRepos.length === 0"
|
||||
v-else-if="filteredProjects.length === 0"
|
||||
class="list-empty"
|
||||
>
|
||||
{{ projects.length === 0 ? '아직 프로젝트가 없습니다. 새 프로젝트를 만들어 보세요.' : '검색 결과가 없습니다.' }}
|
||||
|
||||
@@ -22,7 +22,7 @@ const memberStore = useMemberStore()
|
||||
|
||||
// 프로젝트 헤더 — API 연동
|
||||
const repo = ref<Project | null>(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 },
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
프로젝트 삭제
|
||||
</button>
|
||||
|
||||
@@ -53,14 +53,14 @@ function toDateInput(iso: string): string {
|
||||
|
||||
// 저장소 헤더(빵부스러기) — API 연동
|
||||
const repo = ref<Project | null>(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<User>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// --- 담당자 선택 (저장소 멤버 중) ---
|
||||
// --- 담당자 선택 (프로젝트 멤버 중) ---
|
||||
const assignees = ref<User[]>([])
|
||||
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 ? '프로젝트 멤버가 없습니다.' : '추가할 멤버가 없습니다.' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<string, unknown>, key: string): string {
|
||||
@@ -19,16 +19,16 @@ function pstr(payload: Record<string, unknown>, key: string): string {
|
||||
// 행위자/시간 등 공통 필드
|
||||
function baseOf(
|
||||
api: ApiActivity,
|
||||
): Pick<RepoActivityItem, 'avatarUrl' | 'time'> {
|
||||
): Pick<ProjectActivityItem, 'avatarUrl' | 'time'> {
|
||||
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<RepoActivityDay[]>([])
|
||||
const days = ref<ProjectActivityDay[]>([])
|
||||
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 {
|
||||
|
||||
@@ -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<MemberView[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -46,7 +46,7 @@ export const useMemberStore = defineStore('member', () => {
|
||||
async function fetchList(projectId: string): Promise<void> {
|
||||
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)
|
||||
|
||||
@@ -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<RepoTaskView[]>([])
|
||||
const tasks = ref<ProjectTaskView[]>([])
|
||||
const current = ref<TaskDetailView | null>(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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 내 업무(저장소 횡단) 도메인 타입 — 백엔드 MyTaskRowResponse 와 1:1
|
||||
// 내 업무(프로젝트 횡단) 도메인 타입 — 백엔드 MyTaskRowResponse 와 1:1
|
||||
|
||||
import type { TaskStatus } from '@/mock/relay.mock'
|
||||
import type { ApiMember } from '@/types/repo'
|
||||
|
||||
@@ -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<ApiRepoTask> & {
|
||||
// 업무 목록 응답 — 페이지네이션 + 세그먼트 카운트 동봉 (백엔드 ProjectTaskListResult 와 1:1)
|
||||
export type ApiProjectTaskListResult = Paginated<ApiProjectTask> & {
|
||||
counts: TaskSegmentCounts
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user