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:
2026-06-20 19:37:51 +09:00
parent 6ba148314c
commit 7cd6676ef9
35 changed files with 886 additions and 1709 deletions
+3 -3
View File
@@ -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 }
+1 -1
View File
@@ -46,7 +46,7 @@ export function useAi() {
// 할 일(체크리스트) 추천 — 실패는 호출측에서 인라인 처리(silent)
async function suggestChecklist(payload: {
repoId: string
projectId: string
title: string
content?: string[]
}): Promise<SuggestChecklistResult> {
+10 -10
View File
@@ -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 }
+39
View File
@@ -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 }
}
-50
View File
@@ -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 }
}
+28 -28
View File
@@ -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
}