feat(m4): project & member management with tests and e2e

- ProjectRepository, ProjectMemberRepository, minimal NotificationRepository
- ProjectService: create/update/archive/delete project, add/remove member,
  permission checks (isMember/admin), member-added notification, getDashboard
  skeleton, getMyProjects, getMemberRole
- projectValidator, API services factory
- API routes: projects CRUD, members list/add/remove
- Screens: dashboard (project list + create form), project overview, members,
  settings; layout (Header/Sidebar/ProjectNav) and project components
- Unit tests (Project/ProjectMember repositories, ProjectService),
  integration project-member-permission, e2e project-management
This commit is contained in:
Ken Yasue
2026-06-25 01:02:10 +02:00
parent 8a67523c0e
commit 9eb391ea8d
30 changed files with 2230 additions and 14 deletions

24
lib/api/services.ts Normal file
View File

@ -0,0 +1,24 @@
import { getDb } from '@/lib/db/sqlite';
import { ProjectRepository } from '@/repositories/ProjectRepository';
import { ProjectMemberRepository } from '@/repositories/ProjectMemberRepository';
import { NotificationRepository } from '@/repositories/NotificationRepository';
import { UserRepository } from '@/repositories/UserRepository';
import { ProjectService } from '@/services/ProjectService';
/**
* Route Handler用に各Repository/Serviceを構築するファクトリ。
* 依存はすべて同じgetDb()接続を共有し、トランザクション境界を機能させる。
*/
export function createProjectService(): ProjectService {
const db = getDb();
return new ProjectService(
new ProjectRepository(db),
new ProjectMemberRepository(db),
new NotificationRepository(db),
db
);
}
export function createUserRepository(): UserRepository {
return new UserRepository(getDb());
}

View File

@ -0,0 +1,66 @@
import { ValidationError } from '@/lib/errors';
import type { ProjectStatus } from '@/lib/types';
const MAX_NAME_LENGTH = 200;
const MAX_DESCRIPTION_LENGTH = 2000;
const VALID_STATUSES: ProjectStatus[] = [
'active',
'on_hold',
'completed',
'archived',
];
export interface ProjectCreateInput {
name: string;
description?: string;
}
export interface ProjectUpdateInput {
name?: string;
description?: string;
status?: ProjectStatus;
}
export function validateProjectCreate(input: ProjectCreateInput): void {
if (!input.name || !input.name.trim()) {
throw new ValidationError('プロジェクト名を入力してください', 'name');
}
if (input.name.length > MAX_NAME_LENGTH) {
throw new ValidationError(
`プロジェクト名は${MAX_NAME_LENGTH}文字以内で入力してください`,
'name'
);
}
if (input.description && input.description.length > MAX_DESCRIPTION_LENGTH) {
throw new ValidationError(
`説明文は${MAX_DESCRIPTION_LENGTH}文字以内で入力してください`,
'description'
);
}
}
export function validateProjectUpdate(input: ProjectUpdateInput): void {
if (input.name !== undefined) {
if (!input.name.trim()) {
throw new ValidationError('プロジェクト名を入力してください', 'name');
}
if (input.name.length > MAX_NAME_LENGTH) {
throw new ValidationError(
`プロジェクト名は${MAX_NAME_LENGTH}文字以内で入力してください`,
'name'
);
}
}
if (
input.description !== undefined &&
input.description.length > MAX_DESCRIPTION_LENGTH
) {
throw new ValidationError(
`説明文は${MAX_DESCRIPTION_LENGTH}文字以内で入力してください`,
'description'
);
}
if (input.status !== undefined && !VALID_STATUSES.includes(input.status)) {
throw new ValidationError('無効なステータスです', 'status');
}
}