feat: 프론트엔드 프로젝트 우선 구조 재구성 (Phase 2-a)
- 데이터 계층 신설: types/project·useProject·project.store (프로젝트 CRUD·페이지네이션) - useTask/useMember/useActivity 경로를 /projects/:projectId/... 로 전환 - task·my-task·activity 타입/스토어 repoId/repoName → projectId/projectName - 알림 store 라우팅·문구 프로젝트화, 활동 타입 repo.linked/unlinked·project.* 반영 - 라우팅 /projects/... 전면 교체, / → /projects 리다이렉트 - 페이지 rename Repo*→Project* (List/Create/Detail/Settings/Members/Activity) - ProjectCreatePage: Gitea 저장소 폼 → 단순 프로젝트 생성 폼으로 재작성 - RepoHeader→ProjectHeader, RepoTabs→ProjectTabs, AppShell 네비 프로젝트화 - 죽은 파일 삭제: repo.store·useRepo·GiteaRepoBar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,9 @@ import type { ApiActivity } from '@/types/activity'
|
||||
export function useActivity() {
|
||||
const api = useApi()
|
||||
|
||||
// 저장소 활동 피드(최신순)
|
||||
async function list(repoId: string): Promise<ApiActivity[]> {
|
||||
return (await api.get(`/repos/${repoId}/activity`)) as unknown as ApiActivity[]
|
||||
// 프로젝트 활동 피드(최신순)
|
||||
async function list(projectId: string): Promise<ApiActivity[]> {
|
||||
return (await api.get(`/projects/${projectId}/activity`)) as unknown as ApiActivity[]
|
||||
}
|
||||
|
||||
return { list }
|
||||
|
||||
@@ -46,7 +46,7 @@ export function useAi() {
|
||||
|
||||
// 할 일(체크리스트) 추천 — 실패는 호출측에서 인라인 처리(silent)
|
||||
async function suggestChecklist(payload: {
|
||||
repoId: string
|
||||
projectId: string
|
||||
title: string
|
||||
content?: string[]
|
||||
}): Promise<SuggestChecklistResult> {
|
||||
|
||||
@@ -6,48 +6,48 @@ import type {
|
||||
} from '@/types/member'
|
||||
import type { Paginated } from '@/types/pagination'
|
||||
|
||||
// 저장소 멤버 도메인 API Composable — 인터셉터가 success/data 를 언래핑
|
||||
// 경로는 모두 repoId(slugName) 하위: /repos/:repoId/members
|
||||
// 프로젝트 멤버 도메인 API Composable — 인터셉터가 success/data 를 언래핑
|
||||
// 경로는 모두 projectId 하위: /projects/:projectId/members
|
||||
export function useMember() {
|
||||
const api = useApi()
|
||||
|
||||
// 멤버 목록 (멤버 또는 공개 저장소) — 백엔드가 페이지네이션 { items, total, ... } 반환
|
||||
async function list(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
page = 1,
|
||||
size = 20,
|
||||
): Promise<Paginated<ApiRepoMember>> {
|
||||
return (await api.get(`/repos/${repoId}/members`, {
|
||||
return (await api.get(`/projects/${projectId}/members`, {
|
||||
params: { page, size },
|
||||
})) as unknown as Paginated<ApiRepoMember>
|
||||
}
|
||||
|
||||
// 멤버 초대 (admin) — 가입된 사용자만 이메일로 초대
|
||||
async function invite(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
payload: InviteMemberPayload,
|
||||
): Promise<ApiRepoMember> {
|
||||
return (await api.post(
|
||||
`/repos/${repoId}/members`,
|
||||
`/projects/${projectId}/members`,
|
||||
payload,
|
||||
)) as unknown as ApiRepoMember
|
||||
}
|
||||
|
||||
// 멤버 역할/직무 변경 (admin, owner 제외)
|
||||
async function update(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
userId: string,
|
||||
payload: UpdateMemberPayload,
|
||||
): Promise<ApiRepoMember> {
|
||||
return (await api.patch(
|
||||
`/repos/${repoId}/members/${userId}`,
|
||||
`/projects/${projectId}/members/${userId}`,
|
||||
payload,
|
||||
)) as unknown as ApiRepoMember
|
||||
}
|
||||
|
||||
// 멤버 제거 (admin, owner 제외)
|
||||
async function remove(repoId: string, userId: string): Promise<void> {
|
||||
await api.delete(`/repos/${repoId}/members/${userId}`)
|
||||
async function remove(projectId: string, userId: string): Promise<void> {
|
||||
await api.delete(`/projects/${projectId}/members/${userId}`)
|
||||
}
|
||||
|
||||
return { list, invite, update, remove }
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useApi } from './useApi'
|
||||
import type {
|
||||
ApiProject,
|
||||
CreateProjectPayload,
|
||||
UpdateProjectPayload,
|
||||
} from '@/types/project'
|
||||
import type { Paginated } from '@/types/pagination'
|
||||
|
||||
// 프로젝트 도메인 API Composable — 인터셉터가 success/data 를 언래핑
|
||||
export function useProject() {
|
||||
const api = useApi()
|
||||
|
||||
async function list(page = 1, size = 20): Promise<Paginated<ApiProject>> {
|
||||
return (await api.get('/projects', {
|
||||
params: { page, size },
|
||||
})) as unknown as Paginated<ApiProject>
|
||||
}
|
||||
|
||||
async function get(id: string): Promise<ApiProject> {
|
||||
return (await api.get(`/projects/${id}`)) as unknown as ApiProject
|
||||
}
|
||||
|
||||
async function create(payload: CreateProjectPayload): Promise<ApiProject> {
|
||||
return (await api.post('/projects', payload)) as unknown as ApiProject
|
||||
}
|
||||
|
||||
async function update(
|
||||
id: string,
|
||||
payload: UpdateProjectPayload,
|
||||
): Promise<ApiProject> {
|
||||
return (await api.patch(`/projects/${id}`, payload)) as unknown as ApiProject
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
await api.delete(`/projects/${id}`)
|
||||
}
|
||||
|
||||
return { list, get, create, update, remove }
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useApi } from './useApi'
|
||||
import type {
|
||||
ApiRepo,
|
||||
CreateRepoPayload,
|
||||
RepoCreateMeta,
|
||||
UpdateRepoPayload,
|
||||
} from '@/types/repo'
|
||||
import type { Paginated } from '@/types/pagination'
|
||||
|
||||
// 저장소 도메인 API Composable — 인터셉터가 success/data 를 언래핑
|
||||
export function useRepo() {
|
||||
const api = useApi()
|
||||
|
||||
async function list(page = 1, size = 20): Promise<Paginated<ApiRepo>> {
|
||||
return (await api.get('/repos', {
|
||||
params: { page, size },
|
||||
})) as unknown as Paginated<ApiRepo>
|
||||
}
|
||||
|
||||
// 저장소 생성 폼 메타(소유자/템플릿) — 생성 전 미리 조회
|
||||
async function meta(): Promise<RepoCreateMeta> {
|
||||
return (await api.get('/repos/meta')) as unknown as RepoCreateMeta
|
||||
}
|
||||
|
||||
// 이름(slug) 사용 가능 여부 — 생성 폼 실시간 체크 (실패해도 전역 알림 X)
|
||||
async function checkName(slug: string): Promise<{ available: boolean }> {
|
||||
return (await api.get('/repos/name-available', {
|
||||
params: { slug },
|
||||
silent: true,
|
||||
})) as unknown as { available: boolean }
|
||||
}
|
||||
|
||||
async function get(id: string): Promise<ApiRepo> {
|
||||
return (await api.get(`/repos/${id}`)) as unknown as ApiRepo
|
||||
}
|
||||
|
||||
async function create(payload: CreateRepoPayload): Promise<ApiRepo> {
|
||||
return (await api.post('/repos', payload)) as unknown as ApiRepo
|
||||
}
|
||||
|
||||
async function update(id: string, payload: UpdateRepoPayload): Promise<ApiRepo> {
|
||||
return (await api.patch(`/repos/${id}`, payload)) as unknown as ApiRepo
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
await api.delete(`/repos/${id}`)
|
||||
}
|
||||
|
||||
return { list, meta, checkName, get, create, update, remove }
|
||||
}
|
||||
@@ -13,56 +13,56 @@ import type { TaskStatus } from '@/mock/relay.mock'
|
||||
import { DEFAULT_PAGE_SIZE } from '@/types/pagination'
|
||||
|
||||
// 업무 도메인 API Composable — 인터셉터가 success/data 를 언래핑
|
||||
// 경로는 모두 repoId(slugName) 하위: /repos/:repoId/tasks[/:taskId]
|
||||
// 경로는 모두 projectId 하위: /projects/:projectId/tasks[/:taskId]
|
||||
export function useTask() {
|
||||
const api = useApi()
|
||||
|
||||
// 업무 목록 (세그먼트/검색/담당자 필터 + 페이지네이션) — 응답에 세그먼트 카운트 동봉
|
||||
async function list(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
query: TaskListQuery = {},
|
||||
page = 1,
|
||||
size = DEFAULT_PAGE_SIZE,
|
||||
): Promise<ApiRepoTaskListResult> {
|
||||
return (await api.get(`/repos/${repoId}/tasks`, {
|
||||
return (await api.get(`/projects/${projectId}/tasks`, {
|
||||
params: { ...query, page, size },
|
||||
})) as unknown as ApiRepoTaskListResult
|
||||
}
|
||||
|
||||
// 업무 단건 상세 (taskId = 저장소 내 순번)
|
||||
async function get(repoId: string, taskId: number): Promise<ApiTaskDetail> {
|
||||
return (await api.get(`/repos/${repoId}/tasks/${taskId}`)) as unknown as ApiTaskDetail
|
||||
// 업무 단건 상세 (taskId = 프로젝트 내 순번)
|
||||
async function get(projectId: string, taskId: number): Promise<ApiTaskDetail> {
|
||||
return (await api.get(`/projects/${projectId}/tasks/${taskId}`)) as unknown as ApiTaskDetail
|
||||
}
|
||||
|
||||
// 업무 생성 (admin)
|
||||
async function create(repoId: string, payload: CreateTaskPayload): Promise<ApiTaskDetail> {
|
||||
return (await api.post(`/repos/${repoId}/tasks`, payload)) as unknown as ApiTaskDetail
|
||||
async function create(projectId: string, payload: CreateTaskPayload): Promise<ApiTaskDetail> {
|
||||
return (await api.post(`/projects/${projectId}/tasks`, payload)) as unknown as ApiTaskDetail
|
||||
}
|
||||
|
||||
// 업무 수정 (admin)
|
||||
async function update(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
payload: UpdateTaskPayload,
|
||||
): Promise<ApiTaskDetail> {
|
||||
return (await api.patch(
|
||||
`/repos/${repoId}/tasks/${taskId}`,
|
||||
`/projects/${projectId}/tasks/${taskId}`,
|
||||
payload,
|
||||
)) as unknown as ApiTaskDetail
|
||||
}
|
||||
|
||||
// 업무 삭제 (admin)
|
||||
async function remove(repoId: string, taskId: number): Promise<void> {
|
||||
await api.delete(`/repos/${repoId}/tasks/${taskId}`)
|
||||
async function remove(projectId: string, taskId: number): Promise<void> {
|
||||
await api.delete(`/projects/${projectId}/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
// 상태 변경 (상태 머신)
|
||||
async function changeStatus(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
status: TaskStatus,
|
||||
): Promise<ApiTaskDetail> {
|
||||
return (await api.post(`/repos/${repoId}/tasks/${taskId}/status`, {
|
||||
return (await api.post(`/projects/${projectId}/tasks/${taskId}/status`, {
|
||||
status,
|
||||
})) as unknown as ApiTaskDetail
|
||||
}
|
||||
@@ -70,25 +70,25 @@ export function useTask() {
|
||||
/* ===================== 댓글 / 답글 (5단계) ===================== */
|
||||
|
||||
async function addComment(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
text: string,
|
||||
fileId?: string,
|
||||
): Promise<ApiComment> {
|
||||
return (await api.post(`/repos/${repoId}/tasks/${taskId}/comments`, {
|
||||
return (await api.post(`/projects/${projectId}/tasks/${taskId}/comments`, {
|
||||
text,
|
||||
...(fileId ? { fileId } : {}),
|
||||
})) as unknown as ApiComment
|
||||
}
|
||||
|
||||
async function addReply(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
commentId: string,
|
||||
text: string,
|
||||
): Promise<ApiCommentReply> {
|
||||
return (await api.post(
|
||||
`/repos/${repoId}/tasks/${taskId}/comments/${commentId}/replies`,
|
||||
`/projects/${projectId}/tasks/${taskId}/comments/${commentId}/replies`,
|
||||
{ text },
|
||||
)) as unknown as ApiCommentReply
|
||||
}
|
||||
@@ -96,52 +96,52 @@ export function useTask() {
|
||||
/* ===================== 파일 첨부 (5단계) ===================== */
|
||||
|
||||
async function uploadFile(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
file: File,
|
||||
): Promise<ApiAttachment> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
return (await api.post(`/repos/${repoId}/tasks/${taskId}/files`, form, {
|
||||
return (await api.post(`/projects/${projectId}/tasks/${taskId}/files`, form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})) as unknown as ApiAttachment
|
||||
}
|
||||
|
||||
async function removeFile(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
fileId: string,
|
||||
): Promise<void> {
|
||||
await api.delete(`/repos/${repoId}/tasks/${taskId}/files/${fileId}`)
|
||||
await api.delete(`/projects/${projectId}/tasks/${taskId}/files/${fileId}`)
|
||||
}
|
||||
|
||||
/* ===================== 승인 워크플로우 (5단계) ===================== */
|
||||
|
||||
async function requestApproval(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
note?: string,
|
||||
): Promise<ApiTaskDetail> {
|
||||
return (await api.post(`/repos/${repoId}/tasks/${taskId}/approval/request`, {
|
||||
return (await api.post(`/projects/${projectId}/tasks/${taskId}/approval/request`, {
|
||||
...(note ? { note } : {}),
|
||||
})) as unknown as ApiTaskDetail
|
||||
}
|
||||
|
||||
async function approve(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
): Promise<ApiTaskDetail> {
|
||||
return (await api.post(
|
||||
`/repos/${repoId}/tasks/${taskId}/approval/approve`,
|
||||
`/projects/${projectId}/tasks/${taskId}/approval/approve`,
|
||||
)) as unknown as ApiTaskDetail
|
||||
}
|
||||
|
||||
async function requestChanges(
|
||||
repoId: string,
|
||||
projectId: string,
|
||||
taskId: number,
|
||||
note: string,
|
||||
): Promise<ApiTaskDetail> {
|
||||
return (await api.post(`/repos/${repoId}/tasks/${taskId}/approval/changes`, {
|
||||
return (await api.post(`/projects/${projectId}/tasks/${taskId}/approval/changes`, {
|
||||
note,
|
||||
})) as unknown as ApiTaskDetail
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user