chore: 잔존 '저장소' 주석·문구를 '프로젝트'로 정리

저장소 기능 제거 후 남은 '저장소' 표현을 프로젝트로 통일:
- 백엔드: activity/notification/task 주석·ApiResponse 문구, AI 시스템 프롬프트
  (SUGGEST_SYSTEM_PROMPT 의 저장소→프로젝트, 무의미해진 'slug=영문이름' 지시 제거),
  CONTEXT_REPO_LIMIT→CONTEXT_PROJECT_LIMIT(프로젝트 컨텍스트 한도)
- 프론트: 페이지/스토어 주석 및 TaskCreate/TaskDetail 의 사용자 노출 문구('저장소'→'프로젝트')
- 단, auth OAuth state 'store' 의미의 '저장소'(cookie-state/strategies)는 정당하여 유지

검증: 백엔드 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:57:06 +09:00
parent be0a1bc9d1
commit 32a87d8f62
15 changed files with 42 additions and 36 deletions
@@ -45,7 +45,7 @@ export class Activity {
@JoinColumn({ name: 'project_id' })
project!: Relation<Project>;
// 업무 활동이면 업무 순번(seq) — 업무 상세 활동 탭 필터용. 저장소 활동이면 null
// 업무 활동이면 업무 순번(seq) — 업무 상세 활동 탭 필터용. 프로젝트 활동이면 null
// (Task FK 대신 seq 를 저장해 업무 삭제와 무관하게 활동을 유지)
@Index()
@Column({ name: 'task_seq', type: 'int', nullable: true })
+1 -1
View File
@@ -9,7 +9,7 @@ import { AiConversation } from './entities/ai-conversation.entity';
import { AiMessage } from './entities/ai-message.entity';
// AI 모듈 — Claude(Anthropic) 채팅 프록시 + 대화/메시지 영속.
// Task/ProjectModule 을 가져와 사용자 업무·저장소·프로젝트 컨텍스트를 시스템 프롬프트에 주입한다.
// Task/ProjectModule 을 가져와 사용자 업무·프로젝트 컨텍스트를 시스템 프롬프트에 주입한다.
@Module({
imports: [
TypeOrmModule.forFeature([AiConversation, AiMessage]),
+9 -6
View File
@@ -48,8 +48,8 @@ const MAX_TOKENS = 1024;
const TITLE_MAX = 60;
// 컨텍스트로 주입할 담당 업무 최대 건수(토큰 절약)
const CONTEXT_TASK_LIMIT = 30;
// 컨텍스트로 주입할 저장소 최대 개수
const CONTEXT_REPO_LIMIT = 30;
// 컨텍스트로 주입할 프로젝트 최대 개수
const CONTEXT_PROJECT_LIMIT = 30;
// 주제 한정(가드레일) + 데이터 근거 지시
const SYSTEM_PROMPT = [
@@ -64,7 +64,7 @@ const SYSTEM_PROMPT = [
'- 목록이나 강조가 필요하면 마크다운(굵게 **텍스트**, 목록 - 또는 1.)을 사용하세요. 표·복잡한 서식은 피하세요.',
"- 아래 '사용자 데이터' 에 근거해 답하고, 데이터에 없는 내용은 추측하지 말고 모른다고 하세요.",
"- 마감/일정은 제공된 '오늘 날짜' 를 기준으로 계산하세요.",
'- 저장소는 영문 내부 이름이 아니라 한글 표시 제목으로 설명하세요.',
'- 프로젝트는 한글 표시 제목으로 설명하세요.',
].join('\n');
// 업무 상태 → 한국어 라벨(컨텍스트 표기용)
@@ -79,13 +79,13 @@ const STATUS_LABEL: Record<TaskStatus, string> = {
// 할 일 추천용 시스템 프롬프트 — JSON 으로만 응답하도록 강제
const SUGGEST_SYSTEM_PROMPT = [
'당신은 소프트웨어/업무 기획 보조자입니다.',
"주어진 저장소와 업무 정보를 바탕으로, 이 업무(기능)를 완성하기 위해 필요한 '할 일(체크리스트)' 항목을 제안합니다.",
"주어진 프로젝트와 업무 정보를 바탕으로, 이 업무(기능)를 완성하기 위해 필요한 '할 일(체크리스트)' 항목을 제안합니다.",
'',
'[규칙]',
'- 반드시 아래 JSON 형식으로만 응답하세요. 설명·인사·마크다운 코드펜스 없이 순수 JSON 만 출력합니다.',
'- 카테고리는 "필수 기능", "추가 기능", "보안 기능" 을 기본으로 하되 업무 성격에 맞게 가감하세요.',
'- 각 항목은 한국어로 간결한 한 문장(체크리스트 항목)으로 작성합니다.',
'- 업무·저장소 맥락에 구체적으로 맞추고 일반론은 피하세요. 카테고리당 3~6개가 적당합니다.',
'- 업무·프로젝트 맥락에 구체적으로 맞추고 일반론은 피하세요. 카테고리당 3~6개가 적당합니다.',
'',
'[형식]',
'{"groups":[{"category":"필수 기능","items":["...","..."]},{"category":"보안 기능","items":["..."]}]}',
@@ -367,7 +367,10 @@ export class AiService {
CONTEXT_TASK_LIMIT,
);
const issued = await this.taskService.listIssued(userId, 1, 1);
const projects = await this.projectService.findAll(1, CONTEXT_REPO_LIMIT);
const projects = await this.projectService.findAll(
1,
CONTEXT_PROJECT_LIMIT,
);
const lines = [`오늘 날짜: ${today} (KST)`, ''];
@@ -9,7 +9,7 @@ import {
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
// 알림 타입 — 수신자 관점의 이벤트(활동 피드와 별개: 활동은 저장소 전체 로그, 알림은 개인 수신함)
// 알림 타입 — 수신자 관점의 이벤트(활동 피드와 별개: 활동은 프로젝트 전체 로그, 알림은 개인 수신함)
export type NotificationType =
| 'task.assigned' // 나에게 업무가 지시됨
| 'task.unassigned' // 업무 담당에서 제외됨
@@ -20,9 +20,9 @@ export type NotificationType =
| 'task.due_changed' // 내가 담당한 업무의 마감이 변경됨
| 'task.removed' // 내 관련 업무가 삭제됨
| 'task.commented' // 내 관련 업무에 댓글/답글이 달림
| 'member.invited' // 저장소에 멤버로 초대됨
| 'member.role_changed' // 저장소 내 내 역할이 변경됨
| 'member.removed'; // 저장소에서 제외됨
| 'member.invited' // 프로젝트에 멤버로 초대됨
| 'member.role_changed' // 프로젝트 내 내 역할이 변경됨
| 'member.removed'; // 프로젝트에서 제외됨
// 알림 — 사용자별 수신함. 표시 문구/아이콘은 프론트가 type+payload 로 조립한다.
@Entity('notifications')
+4 -1
View File
@@ -61,7 +61,10 @@ export class TaskController {
summary: '업무 목록 (세그먼트/검색 + 페이지네이션 + 세그먼트 카운트)',
})
@ApiResponse({ status: 200, description: '목록 조회 성공' })
@ApiResponse({ status: 404, description: '저장소를 찾을 수 없음 (RES_001)' })
@ApiResponse({
status: 404,
description: '프로젝트를 찾을 수 없음 (RES_001)',
})
list(
@Param('projectId') projectId: string,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
+3 -3
View File
@@ -312,7 +312,7 @@ export class TaskService {
dto.assigneeIds,
);
// 저장소 내 다음 순번 채번
// 프로젝트 내 다음 순번 채번
const last = await this.taskRepo.findOne({
where: { project: { id: project.id } },
order: { seq: 'DESC' },
@@ -598,7 +598,7 @@ export class TaskService {
taskSeq,
});
// 알림 — 지시자/담당자에게 '업무 삭제'(업무가 사라지므로 프론트는 저장소로 이동)
// 알림 — 지시자/담당자에게 '업무 삭제'(업무가 사라지므로 프론트는 프로젝트로 이동)
await this.notification.notify({
recipientIds: removalRecipients,
actorId: actingUserId,
@@ -1065,7 +1065,7 @@ export class TaskService {
}
}
// projectId 으로 저장소 로딩, 없으면 404
// projectId 으로 프로젝트 로딩, 없으면 404
private async getProjectOrThrow(projectId: string): Promise<Project> {
const project = await this.projectRepo.findOne({
where: { id: projectId },
+1 -1
View File
@@ -11,7 +11,7 @@ import type { Paginated } from '@/types/pagination'
export function useMember() {
const api = useApi()
// 멤버 목록 (멤버 또는 공개 저장소) — 백엔드가 페이지네이션 { items, total, ... } 반환
// 멤버 목록 (멤버 또는 공개 프로젝트) — 백엔드가 페이지네이션 { items, total, ... } 반환
async function list(
projectId: string,
page = 1,
+1 -1
View File
@@ -29,7 +29,7 @@ const verifyBanner = computed<{ ok: boolean; text: string } | null>(() => {
return null
})
// 로그인 처리 — 성공 시 항상 저장소 목록으로 진입(이전 화면/딥링크로 복귀하지 않음)
// 로그인 처리 — 성공 시 항상 프로젝트 목록으로 진입(이전 화면/딥링크로 복귀하지 않음)
async function onLogin() {
if (submitting.value) return
submitting.value = true
@@ -19,7 +19,7 @@ const projectStore = useProjectStore()
const taskStore = useTaskStore()
const { tasks, counts, hasMore, loadingMore, loading } = storeToRefs(taskStore)
// 저장소 헤더/진행률 — API 연동
// 프로젝트 헤더/진행률 — API 연동
const repo = ref<Project | null>(null)
async function loadProject() {
try {
@@ -56,7 +56,7 @@ async function loadMoreTasks() {
}
}
// 저장소 전환 시 헤더 + 목록 리셋 로드
// 프로젝트 전환 시 헤더 + 목록 리셋 로드
watch(
projectId,
() => {
@@ -66,7 +66,7 @@ watch(
{ immediate: true },
)
// 세그먼트 변경 시 목록만 리셋 로드(저장소 재조회 불필요)
// 세그먼트 변경 시 목록만 리셋 로드(프로젝트 재조회 불필요)
watch(segment, loadTasks)
// 검색어는 디바운스 후 리셋 로드(연속 입력 시 호출 폭주 방지)
@@ -116,7 +116,7 @@ const STATUS_LABEL: Record<TaskStatus, string> = {
<b>{{ repo.name }}</b>
</div>
<!-- 저장소 헤더(공통) 진행률 스트립을 extra 슬롯으로 주입 -->
<!-- 프로젝트 헤더(공통) 진행률 스트립을 extra 슬롯으로 주입 -->
<ProjectHeader
:repo="repo"
:meta-text="repo.updatedAgo"
@@ -409,7 +409,7 @@ const STATUS_LABEL: Record<TaskStatus, string> = {
</button>
</div>
<!-- 저장소 찾지 못한 경우 -->
<!-- 프로젝트 찾지 못한 경우 -->
<div
v-else
class="page"
+1 -1
View File
@@ -219,7 +219,7 @@ const filteredProjects = computed(() =>
color: var(--text-2);
}
/* 저장소 행 */
/* 프로젝트 행 */
.repo-row {
display: flex;
align-items: center;
+1 -1
View File
@@ -103,7 +103,7 @@ const points = [
팀에 합류하고<br>업무 지시를 시작하세요
</div>
<div class="bp-sub">
계정을 만들면 저장소 초대되어, 받은 업무를 확인하고 직접 업무를 지시할 있습니다.
계정을 만들면 프로젝트 초대되어, 받은 업무를 확인하고 직접 업무를 지시할 있습니다.
</div>
<div class="bp-points">
<div
+6 -6
View File
@@ -51,7 +51,7 @@ function toDateInput(iso: string): string {
return `${y}-${m}-${day}`
}
// 저장소 헤더(빵부스러기) — API 연동
// 프로젝트 헤더(빵부스러기) — API 연동
const repo = ref<Project | null>(null)
async function loadProject() {
try {
@@ -163,7 +163,7 @@ const aiSuggestions = ref<ChecklistSuggestionGroup[] | null>(null)
const aiError = ref<string | null>(null)
const canRequestAi = computed(() => !aiSuggesting.value && title.value.trim().length > 0)
// 저장소 설명 + 제목/내용을 분석해 카테고리별 할 일 추천을 받는다
// 프로젝트 설명 + 제목/내용을 분석해 카테고리별 할 일 추천을 받는다
async function requestAiSuggestions() {
if (!canRequestAi.value) return
aiSuggesting.value = true
@@ -326,14 +326,14 @@ function goBack() {
to="/projects"
class="lnk"
>
저장소
프로젝트
</RouterLink>
<span class="sep">/</span>
<RouterLink
:to="`/projects/${projectId}`"
class="lnk"
>
{{ repo?.name ?? '저장소' }}
{{ repo?.name ?? '프로젝트' }}
</RouterLink>
<span class="sep">/</span>
{{ isEdit ? '수정' : '새 업무' }}
@@ -353,7 +353,7 @@ function goBack() {
class="btn ai"
type="button"
:disabled="!canRequestAi"
title="저장소·업무 내용을 분석해 할 일을 추천합니다"
title="프로젝트·업무 내용을 분석해 할 일을 추천합니다"
@click="requestAiSuggestions"
>
<svg
@@ -595,7 +595,7 @@ function goBack() {
v-if="aiSuggesting"
class="ai-suggest-msg"
>
저장소 업무 내용을 분석하고 있어요
프로젝트 업무 내용을 분석하고 있어요
</div>
<div
v-else-if="aiError"
+2 -2
View File
@@ -101,7 +101,7 @@ function closeAllOverlays() {
moreOpen.value = false
}
// 모달에서 삭제 확정 → DELETE 후 저장소 상세로 이동
// 모달에서 삭제 확정 → DELETE 후 프로젝트 상세로 이동
async function confirmDelete() {
if (!task.value) return
showDelete.value = false
@@ -320,7 +320,7 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
to="/projects"
class="lnk"
>
저장소
프로젝트
</RouterLink>
<span class="sep">/</span>
<RouterLink
+2 -2
View File
@@ -112,14 +112,14 @@ function toProjectActivityItem(api: ApiActivity): ProjectActivityItem {
}
}
// 활동 스토어 — 저장소 활동 피드(날짜 그룹)
// 활동 스토어 — 프로젝트 활동 피드(날짜 그룹)
export const useActivityStore = defineStore('activity', () => {
const days = ref<ProjectActivityDay[]>([])
const loading = ref(false)
const activityApi = useActivity()
// 저장소 활동 조회 → 날짜 그룹으로 묶기(목록은 최신순이라 같은 날은 연속)
// 프로젝트 활동 조회 → 날짜 그룹으로 묶기(목록은 최신순이라 같은 날은 연속)
async function fetch(projectId: string): Promise<void> {
loading.value = true
try {
+1 -1
View File
@@ -27,7 +27,7 @@ function toMemberView(api: ApiProjectMember, currentUserId: string | null): Memb
}
}
// 프로젝트 멤버 스토어 — 현재 저장소의 멤버 목록 상태 + 초대/역할변경/제거 액션
// 프로젝트 멤버 스토어 — 현재 프로젝트의 멤버 목록 상태 + 초대/역할변경/제거 액션
export const useMemberStore = defineStore('member', () => {
const members = ref<MemberView[]>([])
const loading = ref(false)