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:
2026-06-21 01:52:23 +09:00
parent ef1757181d
commit be0a1bc9d1
22 changed files with 112 additions and 112 deletions
+7 -7
View File
@@ -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 제외)
+1 -1
View File
@@ -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 -3
View File
@@ -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 = 프로젝트 내 순번)
+7 -7
View File
@@ -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 },
+3 -3
View File
@@ -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>
+5 -5
View File
@@ -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 -8
View File
@@ -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 {
+6 -6
View File
@@ -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)
+8 -8
View File
@@ -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
+3 -3
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
// 내 업무(저장소 횡단) 도메인 타입 — 백엔드 MyTaskRowResponse 와 1:1
// 내 업무(프로젝트 횡단) 도메인 타입 — 백엔드 MyTaskRowResponse 와 1:1
import type { TaskStatus } from '@/mock/relay.mock'
import type { ApiMember } from '@/types/repo'
+5 -5
View File
@@ -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
}