diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 18a953e..cd63797 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -689,6 +689,53 @@ paths:
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
+ /team-space/{projectGroupId}/dev-guide/history:
+ get:
+ tags: [TeamSpace]
+ security:
+ - bearerAuth: []
+ summary: Get AI development guide version history
+ parameters:
+ - $ref: '#/components/parameters/ProjectGroupId'
+ responses:
+ '200':
+ description: AI development guide version history
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DevGuideHistoryListResponse'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '409':
+ $ref: '#/components/responses/Conflict'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ /team-space/{projectGroupId}/dev-guide/history/{devGuideId}:
+ get:
+ tags: [TeamSpace]
+ security:
+ - bearerAuth: []
+ summary: Get AI development guide version content
+ parameters:
+ - $ref: '#/components/parameters/ProjectGroupId'
+ - $ref: '#/components/parameters/DevGuideId'
+ responses:
+ '200':
+ description: AI development guide version content
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DevGuideHistoryContentResponse'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
/team-space/{projectGroupId}/dev-guide/regenerate:
post:
tags: [TeamSpace]
@@ -724,6 +771,28 @@ paths:
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/InternalServerError'
+ /team-space/{projectGroupId}/dev-guide/{devGuideId}/confirm:
+ post:
+ tags: [TeamSpace]
+ security:
+ - bearerAuth: []
+ summary: Confirm an AI development guide version
+ parameters:
+ - $ref: '#/components/parameters/ProjectGroupId'
+ - $ref: '#/components/parameters/DevGuideId'
+ responses:
+ '200':
+ description: AI development guide version confirmed
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+ '409':
+ $ref: '#/components/responses/Conflict'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
/team-space/{projectGroupId}/github/status:
get:
tags: [TeamSpace]
@@ -905,6 +974,30 @@ paths:
$ref: '#/components/responses/NotFound'
'502':
$ref: '#/components/responses/BadGateway'
+ /team-space/{projectGroupId}/github/users/{targetUserId}/weekly-summaries:
+ get:
+ tags: [TeamSpace]
+ security:
+ - bearerAuth: []
+ summary: Get weekly GitHub activity summaries for a team member
+ parameters:
+ - $ref: '#/components/parameters/ProjectGroupId'
+ - $ref: '#/components/parameters/TargetUserId'
+ responses:
+ '200':
+ description: Weekly GitHub summaries
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GithubWeeklySummaryListResponse'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
components:
securitySchemes:
bearerAuth:
@@ -933,6 +1026,13 @@ components:
schema:
type: integer
format: int64
+ DevGuideId:
+ in: path
+ name: devGuideId
+ required: true
+ schema:
+ type: integer
+ format: int64
GithubRepositoryId:
in: path
name: githubRepositoryId
@@ -1589,6 +1689,75 @@ components:
contributionScore:
type: integer
format: int64
+ GithubWeeklySummaryListResponse:
+ type: object
+ additionalProperties: false
+ required: [summaries]
+ properties:
+ summaries:
+ type: array
+ items:
+ $ref: '#/components/schemas/GithubWeeklySummary'
+ GithubWeeklySummary:
+ type: object
+ additionalProperties: false
+ required:
+ - weeklyGithubSummaryId
+ - periodStart
+ - periodEnd
+ - sourcePrCount
+ - sourceIssueCount
+ - summary
+ properties:
+ weeklyGithubSummaryId:
+ type: integer
+ format: int64
+ periodStart:
+ type: string
+ format: date-time
+ periodEnd:
+ type: string
+ format: date-time
+ sourcePrCount:
+ type: integer
+ format: int64
+ sourceIssueCount:
+ type: integer
+ format: int64
+ summary:
+ $ref: '#/components/schemas/GithubWeeklySummaryContent'
+ GithubWeeklySummaryContent:
+ type: object
+ additionalProperties: false
+ required:
+ - summary
+ - mainActivities
+ - pullRequestHighlights
+ - issueHighlights
+ - followUpSuggestions
+ properties:
+ summary:
+ type: string
+ mainActivities:
+ type: array
+ maxItems: 5
+ items:
+ type: string
+ pullRequestHighlights:
+ type: array
+ maxItems: 5
+ items:
+ type: string
+ issueHighlights:
+ type: array
+ maxItems: 5
+ items:
+ type: string
+ followUpSuggestions:
+ type: array
+ maxItems: 3
+ items:
+ type: string
DevGuideGenerationStatus:
type: string
enum: [GENERATING, COMPLETED, FAILED]
@@ -1654,6 +1823,85 @@ components:
type:
- integer
- 'null'
+ DevGuideHistory:
+ type: object
+ additionalProperties: false
+ required: [devGuideId, versionNo, generationType, confirmed, createdAt]
+ properties:
+ devGuideId:
+ type: integer
+ format: int64
+ versionNo:
+ type: integer
+ generationType:
+ $ref: '#/components/schemas/DevGuideGenerationType'
+ confirmed:
+ type: boolean
+ createdAt:
+ type: string
+ format: date-time
+ DevGuideHistoryListResponse:
+ type: object
+ additionalProperties: false
+ required: [histories]
+ properties:
+ histories:
+ type: array
+ items:
+ $ref: '#/components/schemas/DevGuideHistory'
+ DevGuideHistoryContentResponse:
+ type: object
+ additionalProperties: false
+ required:
+ - devGuideId
+ - versionNo
+ - generationType
+ - confirmed
+ - createdAt
+ - overview
+ - techStack
+ - mvpPriorities
+ - decisionPoints
+ - milestones
+ properties:
+ devGuideId:
+ type: integer
+ format: int64
+ versionNo:
+ type: integer
+ generationType:
+ $ref: '#/components/schemas/DevGuideGenerationType'
+ confirmed:
+ type: boolean
+ createdAt:
+ type: string
+ format: date-time
+ overview:
+ type: string
+ techStack:
+ type: array
+ minItems: 5
+ maxItems: 7
+ items:
+ $ref: '#/components/schemas/DevGuideTechStackItem'
+ mvpPriorities:
+ type: array
+ minItems: 3
+ maxItems: 3
+ items:
+ $ref: '#/components/schemas/DevGuideMvpPriority'
+ decisionPoints:
+ type: array
+ minItems: 3
+ maxItems: 5
+ items:
+ $ref: '#/components/schemas/DevGuideDecisionPoint'
+ milestones:
+ type: array
+ minItems: 12
+ maxItems: 12
+ items:
+ $ref: '#/components/schemas/DevGuideMilestone'
DevGuideContent:
type: object
additionalProperties: false
diff --git a/src/features/team/components/real-github-installation-panel.tsx b/src/features/team/components/real-github-installation-panel.tsx
index e5da9ca..d1985a4 100644
--- a/src/features/team/components/real-github-installation-panel.tsx
+++ b/src/features/team/components/real-github-installation-panel.tsx
@@ -9,6 +9,7 @@ import {
Save,
Sparkles,
Trophy,
+ UserRound,
type LucideIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@@ -28,16 +29,22 @@ import {
useGithubInstallationStatusQuery,
useGithubRepositoriesQuery,
useGithubRepositoryContributionsQuery,
+ useGithubWeeklySummariesQuery,
useSetGithubRepositoriesMutation,
useSyncGithubPullRequestContributionsMutation,
} from "@/features/team/hooks/use-team-space-queries";
import { getApiErrorMessage } from "@/lib/api/client";
-import type { MyProjectGroup } from "@/lib/types/project-group";
+import type {
+ MyProjectGroup,
+ ProjectGroupMember,
+} from "@/lib/types/project-group";
import type {
GithubRepository,
GithubRepositoryContributor,
+ GithubWeeklySummary,
} from "@/lib/types/team-space";
import { cn } from "@/lib/utils";
+import { formatDateTime } from "@/lib/utils/date";
const contributionNumberFormatter = new Intl.NumberFormat("ko-KR");
@@ -435,11 +442,200 @@ export function RealGithubInstallationPanel({
) : null}
+
+
);
}
+function RealGithubWeeklySummariesPanel({
+ isGithubConnected,
+ members,
+ projectGroupId,
+}: {
+ isGithubConnected: boolean;
+ members: ProjectGroupMember[];
+ projectGroupId: number;
+}) {
+ return (
+
{contributionNumberFormatter.format(value)}
diff --git a/src/features/team/components/real-team-guide-panel.tsx b/src/features/team/components/real-team-guide-panel.tsx
index c5dc2ab..8515015 100644
--- a/src/features/team/components/real-team-guide-panel.tsx
+++ b/src/features/team/components/real-team-guide-panel.tsx
@@ -1,10 +1,20 @@
-import { CheckCircle2, LoaderCircle, RefreshCw } from "lucide-react";
+import {
+ Check,
+ CheckCircle2,
+ History,
+ LoaderCircle,
+ RefreshCw,
+} from "lucide-react";
+import { useState } from "react";
import { AppPanel, AppPanelHeader } from "@/components/app-shell";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { RealInlineStatus } from "@/features/team/components/real-team-shared";
import {
+ useConfirmDevGuideMutation,
+ useDevGuideHistoriesQuery,
+ useDevGuideHistoryContentQuery,
useDevGuideQuery,
useRegenerateDevGuideMutation,
} from "@/features/team/hooks/use-team-space-queries";
@@ -13,14 +23,19 @@ import type { MyProjectGroup } from "@/lib/types/project-group";
import type {
DevGuideContent,
DevGuideGenerationStatus,
+ DevGuideHistory,
DevGuideQueryResponse,
} from "@/lib/types/team-space";
+import { formatDateTime } from "@/lib/utils/date";
export function RealGuidePanel({
projectGroup,
}: {
projectGroup: MyProjectGroup;
}) {
+ const [selectedHistoryId, setSelectedHistoryId] = useState(
+ null,
+ );
const devGuideQuery = useDevGuideQuery(projectGroup.projectGroupId);
const regenerateDevGuideMutation = useRegenerateDevGuideMutation();
const guideResponse = devGuideQuery.data;
@@ -28,6 +43,21 @@ export function RealGuidePanel({
const generationStatus = guideResponse?.generationStatus;
const remainingRegenerationCount =
guideResponse?.remainingRegenerationCount ?? null;
+ const historiesQuery = useDevGuideHistoriesQuery(
+ projectGroup.projectGroupId,
+ Boolean(guideResponse) && generationStatus !== "GENERATING",
+ );
+ const histories = historiesQuery.data?.histories ?? [];
+ const selectedHistory =
+ histories.find((history) => history.devGuideId === selectedHistoryId) ??
+ histories[0] ??
+ null;
+ const historyContentQuery = useDevGuideHistoryContentQuery(
+ projectGroup.projectGroupId,
+ selectedHistory?.devGuideId,
+ Boolean(selectedHistory),
+ );
+ const confirmDevGuideMutation = useConfirmDevGuideMutation();
const handleRegenerateDevGuide = () => {
regenerateDevGuideMutation.mutate({
@@ -35,6 +65,13 @@ export function RealGuidePanel({
});
};
+ const handleConfirmDevGuide = (devGuideId: number) => {
+ confirmDevGuideMutation.mutate({
+ devGuideId,
+ projectGroupId: projectGroup.projectGroupId,
+ });
+ };
+
if (devGuideQuery.isLoading) {
return (
@@ -225,6 +262,20 @@ export function RealGuidePanel({
+
@@ -470,6 +521,217 @@ function RealDevGuideMilestonePanel({ guide }: { guide: DevGuideContent }) {
);
}
+function RealDevGuideHistoryPanel({
+ confirmError,
+ histories,
+ historiesError,
+ isConfirming,
+ isLoading,
+ onConfirm,
+ onSelectHistory,
+ selectedHistory,
+ selectedHistoryContent,
+ selectedHistoryContentError,
+ selectedHistoryContentLoading,
+}: {
+ confirmError: unknown;
+ histories: DevGuideHistory[];
+ historiesError: unknown;
+ isConfirming: boolean;
+ isLoading: boolean;
+ onConfirm: (devGuideId: number) => void;
+ onSelectHistory: (devGuideId: number) => void;
+ selectedHistory: DevGuideHistory | null;
+ selectedHistoryContent: (DevGuideContent & DevGuideHistory) | null;
+ selectedHistoryContentError: unknown;
+ selectedHistoryContentLoading: boolean;
+}) {
+ const canConfirmSelected = selectedHistory
+ ? !selectedHistory.confirmed
+ : false;
+
+ return (
+
+ 버전 {histories.length}개}
+ description="서버에 저장된 개발 가이드 버전을 비교하고 팀 기준 버전으로 확정해요."
+ eyebrow="History"
+ title="가이드 버전 기록"
+ />
+
+
+ {isLoading ? (
+ }
+ message="가이드 버전 기록을 불러오고 있어요."
+ />
+ ) : null}
+
+ {historiesError ? (
+
+ ) : null}
+
+ {!isLoading && !historiesError && histories.length === 0 ? (
+
+ ) : null}
+
+ {histories.map((history) => (
+
+ ))}
+
+
+
+
+
+
+ {selectedHistory
+ ? `v${selectedHistory.versionNo} 본문`
+ : "선택된 버전 없음"}
+
+
+ {selectedHistory
+ ? formatDateTime(selectedHistory.createdAt)
+ : "기록을 선택하면 본문 요약이 표시돼요."}
+
+
+
+
+
+ {confirmError ? (
+
+ ) : null}
+
+ {selectedHistoryContentLoading ? (
+
}
+ message="선택한 버전의 본문을 불러오고 있어요."
+ />
+ ) : null}
+
+ {selectedHistoryContentError ? (
+
+ ) : null}
+
+ {selectedHistoryContent ? (
+
+
+ {selectedHistoryContent.overview}
+
+
+
+
+
+
+
+ ) : null}
+
+
+
+ );
+}
+
+function RealDevGuideHistoryMetric({
+ label,
+ value,
+}: {
+ label: string;
+ value: string;
+}) {
+ return (
+
+
{label}
+
+ {value}
+
+
+ );
+}
+
+function getDevGuideGenerationTypeLabel(
+ type: DevGuideHistory["generationType"],
+) {
+ if (type === "MANUAL") {
+ return "수동";
+ }
+
+ if (type === "RECOVERY") {
+ return "복구";
+ }
+
+ return "초기";
+}
+
function RealDevGuideRoleTask({
label,
value,
diff --git a/src/features/team/hooks/use-team-space-queries.ts b/src/features/team/hooks/use-team-space-queries.ts
index 1dc28a2..2a268a8 100644
--- a/src/features/team/hooks/use-team-space-queries.ts
+++ b/src/features/team/hooks/use-team-space-queries.ts
@@ -2,18 +2,23 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError } from "@/lib/api/client";
import {
+ confirmDevGuide,
completeGithubAppInstallation,
createGithubAppInstallationUrl,
getAvailableGithubRepositories,
getDevGuide,
+ getDevGuideHistories,
+ getDevGuideHistoryContent,
getGithubInstallationStatus,
getGithubRepositories,
getGithubRepositoryContributions,
+ getGithubWeeklySummaries,
regenerateDevGuide,
setGithubRepositories,
syncGithubPullRequestContributions,
} from "@/lib/api/team-space";
import type {
+ ConfirmDevGuideRequest,
DevGuideQueryResponse,
GithubAppInstallationCompleteRequest,
GithubRepositoryContributionRequest,
@@ -25,6 +30,10 @@ export const teamSpaceQueryKeys = {
all: ["team-space"] as const,
devGuide: (projectGroupId: number) =>
["team-space", projectGroupId, "dev-guide"] as const,
+ devGuideHistories: (projectGroupId: number) =>
+ ["team-space", projectGroupId, "dev-guide", "history"] as const,
+ devGuideHistoryContent: (projectGroupId: number, devGuideId: number) =>
+ ["team-space", projectGroupId, "dev-guide", "history", devGuideId] as const,
disabled: ["team-space", "disabled"] as const,
githubAvailableRepositories: (projectGroupId: number) =>
["team-space", projectGroupId, "github", "available-repositories"] as const,
@@ -42,6 +51,15 @@ export const teamSpaceQueryKeys = {
githubRepositoryId,
"contributions",
] as const,
+ githubWeeklySummaries: (projectGroupId: number, targetUserId: number) =>
+ [
+ "team-space",
+ projectGroupId,
+ "github",
+ "users",
+ targetUserId,
+ "weekly-summaries",
+ ] as const,
githubStatus: (projectGroupId: number) =>
["team-space", projectGroupId, "github", "status"] as const,
};
@@ -60,6 +78,14 @@ function requireProjectGroupId(projectGroupId: number | undefined) {
return projectGroupId;
}
+function requireNumericId(value: number | undefined, label: string) {
+ if (typeof value !== "number") {
+ throw new Error(`${label} is required.`);
+ }
+
+ return value;
+}
+
export function useGithubInstallationStatusQuery(
projectGroupId: number | undefined,
enabled = true,
@@ -124,6 +150,76 @@ export function useRegenerateDevGuideMutation() {
response.remainingRegenerationCount ?? null,
},
);
+ queryClient.invalidateQueries({
+ queryKey: teamSpaceQueryKeys.devGuideHistories(
+ variables.projectGroupId,
+ ),
+ });
+ },
+ });
+}
+
+export function useDevGuideHistoriesQuery(
+ projectGroupId: number | undefined,
+ enabled = true,
+) {
+ return useQuery({
+ enabled: enabled && typeof projectGroupId === "number",
+ queryFn: () => getDevGuideHistories(requireProjectGroupId(projectGroupId)),
+ queryKey:
+ typeof projectGroupId === "number"
+ ? teamSpaceQueryKeys.devGuideHistories(projectGroupId)
+ : teamSpaceQueryKeys.disabled,
+ refetchOnWindowFocus: false,
+ retry: false,
+ staleTime: devGuideStaleTimeMs,
+ });
+}
+
+export function useDevGuideHistoryContentQuery(
+ projectGroupId: number | undefined,
+ devGuideId: number | undefined,
+ enabled = true,
+) {
+ const hasIds =
+ typeof projectGroupId === "number" && typeof devGuideId === "number";
+
+ return useQuery({
+ enabled: enabled && hasIds,
+ queryFn: () =>
+ getDevGuideHistoryContent({
+ devGuideId: requireNumericId(devGuideId, "Dev guide id"),
+ projectGroupId: requireProjectGroupId(projectGroupId),
+ }),
+ queryKey: hasIds
+ ? teamSpaceQueryKeys.devGuideHistoryContent(projectGroupId, devGuideId)
+ : teamSpaceQueryKeys.disabled,
+ refetchOnWindowFocus: false,
+ retry: false,
+ staleTime: devGuideStaleTimeMs,
+ });
+}
+
+export function useConfirmDevGuideMutation() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (payload: ConfirmDevGuideRequest) => confirmDevGuide(payload),
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: teamSpaceQueryKeys.devGuide(variables.projectGroupId),
+ });
+ queryClient.invalidateQueries({
+ queryKey: teamSpaceQueryKeys.devGuideHistories(
+ variables.projectGroupId,
+ ),
+ });
+ queryClient.invalidateQueries({
+ queryKey: teamSpaceQueryKeys.devGuideHistoryContent(
+ variables.projectGroupId,
+ variables.devGuideId,
+ ),
+ });
},
});
}
@@ -262,3 +358,27 @@ export function useSyncGithubPullRequestContributionsMutation() {
},
});
}
+
+export function useGithubWeeklySummariesQuery(
+ projectGroupId: number | undefined,
+ targetUserId: number | undefined,
+ enabled = true,
+) {
+ const hasIds =
+ typeof projectGroupId === "number" && typeof targetUserId === "number";
+
+ return useQuery({
+ enabled: enabled && hasIds,
+ queryFn: () =>
+ getGithubWeeklySummaries({
+ projectGroupId: requireProjectGroupId(projectGroupId),
+ targetUserId: requireNumericId(targetUserId, "Target user id"),
+ }),
+ queryKey: hasIds
+ ? teamSpaceQueryKeys.githubWeeklySummaries(projectGroupId, targetUserId)
+ : teamSpaceQueryKeys.disabled,
+ refetchOnWindowFocus: false,
+ retry: false,
+ staleTime: githubContributionStaleTimeMs,
+ });
+}
diff --git a/src/lib/api/mocks/handlers.ts b/src/lib/api/mocks/handlers.ts
index db4f2a5..250efcd 100644
--- a/src/lib/api/mocks/handlers.ts
+++ b/src/lib/api/mocks/handlers.ts
@@ -39,11 +39,13 @@ import type { MyProjectGroup } from "@/lib/types/project-group";
import type {
DevGuideContent,
DevGuideGenerationStatus,
+ DevGuideHistoryContentResponse,
DevGuideQueryResponse,
GithubAppInstallationCompleteRequest,
GithubInstallationStatus,
GithubRepository,
GithubRepositoryContributionResponse,
+ GithubWeeklySummaryListResponse,
RegenerateDevGuideResponse,
} from "@/lib/types/team-space";
import type {
@@ -72,6 +74,8 @@ let activeDevGuideGenerationStatus: DevGuideGenerationStatus | null =
let remainingDevGuideRegenerationCount: number | null = activeDevGuide
? 3
: null;
+let activeDevGuideHistories = createMockDevGuideHistories(activeDevGuide);
+let nextDevGuideId = 902;
let nextProjectChecklistId = 103;
let githubInstallationStatus: GithubInstallationStatus =
createDisconnectedGithubStatus();
@@ -272,6 +276,8 @@ function resetTeamSpaceApiState() {
activeDevGuide = createMockDevGuide(activeProjectGroup);
activeDevGuideGenerationStatus = activeDevGuide ? "COMPLETED" : null;
remainingDevGuideRegenerationCount = activeDevGuide ? 3 : null;
+ activeDevGuideHistories = createMockDevGuideHistories(activeDevGuide);
+ nextDevGuideId = 902;
nextProjectChecklistId = 103;
githubInstallationStatus = createDisconnectedGithubStatus();
clearPendingGithubInstallationState();
@@ -310,6 +316,106 @@ function createRegeneratedMockDevGuide(feedback: string | undefined) {
} satisfies DevGuideContent;
}
+function createMockDevGuideHistories(
+ guide: DevGuideContent | null,
+): DevGuideHistoryContentResponse[] {
+ if (!guide) {
+ return [];
+ }
+
+ return [
+ {
+ ...guide,
+ confirmed: true,
+ createdAt: "2026-05-24T09:00:00Z",
+ devGuideId: 901,
+ generationType: "INITIAL",
+ versionNo: 2,
+ },
+ {
+ ...guide,
+ confirmed: false,
+ createdAt: "2026-05-20T09:00:00Z",
+ devGuideId: 900,
+ generationType: "INITIAL",
+ overview: `${guide.overview} 초기 버전은 체크리스트 운영보다 팀 규칙 정리에 더 무게를 두었습니다.`,
+ versionNo: 1,
+ },
+ ];
+}
+
+function addMockDevGuideHistory({
+ content,
+ generationType,
+}: {
+ content: DevGuideContent;
+ generationType: DevGuideHistoryContentResponse["generationType"];
+}) {
+ const nextVersionNo =
+ Math.max(
+ 0,
+ ...activeDevGuideHistories.map((history) => history.versionNo),
+ ) + 1;
+
+ activeDevGuideHistories = [
+ {
+ ...content,
+ confirmed: false,
+ createdAt: new Date().toISOString(),
+ devGuideId: nextDevGuideId,
+ generationType,
+ versionNo: nextVersionNo,
+ },
+ ...activeDevGuideHistories,
+ ];
+ nextDevGuideId += 1;
+}
+
+function createMockGithubWeeklySummaries(
+ targetUserId: number,
+): GithubWeeklySummaryListResponse {
+ const member = activeProjectGroup?.members.find(
+ (candidate) => candidate.userId === targetUserId,
+ );
+
+ if (!member) {
+ return { summaries: [] };
+ }
+
+ const roleLabel = member.memberRole.toLowerCase();
+
+ return {
+ summaries: [
+ {
+ periodEnd: "2026-06-07T14:59:59Z",
+ periodStart: "2026-06-01T15:00:00Z",
+ sourceIssueCount: 3,
+ sourcePrCount: 4,
+ summary: {
+ followUpSuggestions: [
+ "리뷰 대기 PR의 merge 기준을 팀 체크리스트에 남겨요.",
+ "다음 주에는 변경 파일이 큰 PR을 더 작게 나눠요.",
+ ],
+ issueHighlights: [
+ "매칭 플로우 예외 케이스를 이슈로 정리했어요.",
+ "팀 스페이스 진입 조건을 재확인했어요.",
+ ],
+ mainActivities: [
+ `${member.nickname}님은 ${roleLabel} 작업 흐름을 안정화했어요.`,
+ "팀 스페이스 API 계약과 화면 상태를 함께 점검했어요.",
+ ],
+ pullRequestHighlights: [
+ "GitHub 연동 상태 카드의 실패/로딩 표시를 다듬었어요.",
+ "체크리스트 API 응답과 mock 데이터를 맞췄어요.",
+ ],
+ summary: `${member.nickname}님은 이번 주 팀 운영 기능의 연결성과 검증 흐름을 개선했습니다.`,
+ },
+ weeklyGithubSummaryId: targetUserId * 1000 + 1,
+ },
+ ],
+ };
+}
+
async function readOptionalJsonBody
(request: Request) {
try {
return (await request.json()) as T;
@@ -2002,6 +2108,77 @@ export const handlers = [
},
),
+ http.get(
+ getPath("/team-space/:projectGroupId/dev-guide/history"),
+ async ({ params, request }) => {
+ await delay(250);
+ syncSessionFromRequest(request);
+
+ const projectGroupId = Number(params.projectGroupId);
+ const accessError = assertProjectGroupAccess(projectGroupId);
+
+ if (accessError) {
+ return accessError;
+ }
+
+ if (activeDevGuideGenerationStatus === "GENERATING") {
+ return buildErrorResponse(
+ 409,
+ "개발 가이드라인이 생성 중입니다.",
+ "DEV_GUIDE_GENERATING",
+ );
+ }
+
+ return HttpResponse.json({
+ histories: activeDevGuideHistories.map(
+ ({
+ confirmed,
+ createdAt,
+ devGuideId,
+ generationType,
+ versionNo,
+ }) => ({
+ confirmed,
+ createdAt,
+ devGuideId,
+ generationType,
+ versionNo,
+ }),
+ ),
+ });
+ },
+ ),
+
+ http.get(
+ getPath("/team-space/:projectGroupId/dev-guide/history/:devGuideId"),
+ async ({ params, request }) => {
+ await delay(250);
+ syncSessionFromRequest(request);
+
+ const projectGroupId = Number(params.projectGroupId);
+ const devGuideId = Number(params.devGuideId);
+ const accessError = assertProjectGroupAccess(projectGroupId);
+
+ if (accessError) {
+ return accessError;
+ }
+
+ const history = activeDevGuideHistories.find(
+ (candidate) => candidate.devGuideId === devGuideId,
+ );
+
+ if (!history) {
+ return buildErrorResponse(
+ 404,
+ "개발 가이드라인이 존재하지 않습니다.",
+ "DEV_GUIDE_NOT_FOUND",
+ );
+ }
+
+ return HttpResponse.json(history);
+ },
+ ),
+
http.post(
getPath("/team-space/:projectGroupId/dev-guide/regenerate"),
async ({ params, request }) => {
@@ -2070,6 +2247,10 @@ export const handlers = [
remainingDevGuideRegenerationCount =
remainingDevGuideRegenerationCount ?? 3;
}
+ addMockDevGuideHistory({
+ content: activeDevGuide,
+ generationType,
+ });
return HttpResponse.json({
content: activeDevGuide,
@@ -2079,6 +2260,56 @@ export const handlers = [
},
),
+ http.post(
+ getPath("/team-space/:projectGroupId/dev-guide/:devGuideId/confirm"),
+ async ({ params, request }) => {
+ await delay(350);
+ syncSessionFromRequest(request);
+
+ const projectGroupId = Number(params.projectGroupId);
+ const devGuideId = Number(params.devGuideId);
+ const accessError = assertProjectGroupAccess(projectGroupId);
+
+ if (accessError) {
+ return accessError;
+ }
+
+ if (activeDevGuideGenerationStatus === "GENERATING") {
+ return buildErrorResponse(
+ 409,
+ "개발 가이드라인이 생성 중입니다.",
+ "DEV_GUIDE_GENERATING",
+ );
+ }
+
+ const history = activeDevGuideHistories.find(
+ (candidate) => candidate.devGuideId === devGuideId,
+ );
+
+ if (!history) {
+ return buildErrorResponse(
+ 404,
+ "개발 가이드라인이 존재하지 않습니다.",
+ "DEV_GUIDE_NOT_FOUND",
+ );
+ }
+
+ activeDevGuideHistories = activeDevGuideHistories.map((candidate) => ({
+ ...candidate,
+ confirmed: candidate.devGuideId === devGuideId,
+ }));
+ activeDevGuide = {
+ decisionPoints: history.decisionPoints,
+ milestones: history.milestones,
+ mvpPriorities: history.mvpPriorities,
+ overview: history.overview,
+ techStack: history.techStack,
+ };
+
+ return new HttpResponse(null, { status: 200 });
+ },
+ ),
+
http.get(
getPath("/team-space/:projectGroupId/github/status"),
async ({ params, request }) => {
@@ -2374,4 +2605,36 @@ export const handlers = [
return new HttpResponse(null, { status: 200 });
},
),
+
+ http.get(
+ getPath(
+ "/team-space/:projectGroupId/github/users/:targetUserId/weekly-summaries",
+ ),
+ async ({ params, request }) => {
+ await delay(300);
+ syncSessionFromRequest(request);
+
+ const projectGroupId = Number(params.projectGroupId);
+ const targetUserId = Number(params.targetUserId);
+ const accessError = assertProjectGroupAccess(projectGroupId);
+
+ if (accessError) {
+ return accessError;
+ }
+
+ const targetMember = activeProjectGroup?.members.find(
+ (member) => member.userId === targetUserId,
+ );
+
+ if (!targetMember) {
+ return buildErrorResponse(
+ 404,
+ "팀 멤버를 찾을 수 없습니다.",
+ "PROJECT_GROUP_MEMBER_NOT_FOUND",
+ );
+ }
+
+ return HttpResponse.json(createMockGithubWeeklySummaries(targetUserId));
+ },
+ ),
];
diff --git a/src/lib/api/team-space.contract.ts b/src/lib/api/team-space.contract.ts
new file mode 100644
index 0000000..4b64f4b
--- /dev/null
+++ b/src/lib/api/team-space.contract.ts
@@ -0,0 +1,33 @@
+import {
+ confirmDevGuide,
+ getDevGuideHistories,
+ getDevGuideHistoryContent,
+ getGithubWeeklySummaries,
+} from "@/lib/api/team-space";
+import type {
+ ConfirmDevGuideRequest,
+ DevGuideHistoryContentResponse,
+ DevGuideHistoryListResponse,
+ GithubWeeklySummaryListResponse,
+ GithubWeeklySummaryRequest,
+} from "@/lib/types/team-space";
+
+type TeamSpaceServerOnlyApiContract = {
+ confirmDevGuide: (payload: ConfirmDevGuideRequest) => Promise;
+ getDevGuideHistories: (
+ projectGroupId: number,
+ ) => Promise;
+ getDevGuideHistoryContent: (
+ payload: ConfirmDevGuideRequest,
+ ) => Promise;
+ getGithubWeeklySummaries: (
+ payload: GithubWeeklySummaryRequest,
+ ) => Promise;
+};
+
+export const teamSpaceServerOnlyApiContract = {
+ confirmDevGuide,
+ getDevGuideHistories,
+ getDevGuideHistoryContent,
+ getGithubWeeklySummaries,
+} satisfies TeamSpaceServerOnlyApiContract;
diff --git a/src/lib/api/team-space.ts b/src/lib/api/team-space.ts
index 45c6a7d..d2526e1 100644
--- a/src/lib/api/team-space.ts
+++ b/src/lib/api/team-space.ts
@@ -1,5 +1,8 @@
import { apiRequest } from "@/lib/api/client";
import type {
+ ConfirmDevGuideRequest,
+ DevGuideHistoryContentResponse,
+ DevGuideHistoryListResponse,
DevGuideQueryResponse,
GithubAppInstallationCompleteRequest,
GithubAppInstallationUrl,
@@ -7,6 +10,8 @@ import type {
GithubRepositoryContributionRequest,
GithubRepositoryContributionResponse,
GithubRepositoryListResponse,
+ GithubWeeklySummaryListResponse,
+ GithubWeeklySummaryRequest,
RegenerateDevGuideRequest,
RegenerateDevGuideResponse,
SetGithubRepositoriesRequest,
@@ -33,6 +38,33 @@ export function regenerateDevGuide({
);
}
+export function getDevGuideHistories(projectGroupId: number) {
+ return apiRequest(
+ `/team-space/${projectGroupId}/dev-guide/history`,
+ );
+}
+
+export function getDevGuideHistoryContent({
+ devGuideId,
+ projectGroupId,
+}: ConfirmDevGuideRequest) {
+ return apiRequest(
+ `/team-space/${projectGroupId}/dev-guide/history/${devGuideId}`,
+ );
+}
+
+export function confirmDevGuide({
+ devGuideId,
+ projectGroupId,
+}: ConfirmDevGuideRequest) {
+ return apiRequest(
+ `/team-space/${projectGroupId}/dev-guide/${devGuideId}/confirm`,
+ {
+ method: "POST",
+ },
+ );
+}
+
export function getGithubInstallationStatus(projectGroupId: number) {
return apiRequest(
`/team-space/${projectGroupId}/github/status`,
@@ -111,3 +143,12 @@ export function syncGithubPullRequestContributions({
},
);
}
+
+export function getGithubWeeklySummaries({
+ projectGroupId,
+ targetUserId,
+}: GithubWeeklySummaryRequest) {
+ return apiRequest(
+ `/team-space/${projectGroupId}/github/users/${targetUserId}/weekly-summaries`,
+ );
+}
diff --git a/src/lib/types/team-space.ts b/src/lib/types/team-space.ts
index 5dfac02..134efb3 100644
--- a/src/lib/types/team-space.ts
+++ b/src/lib/types/team-space.ts
@@ -112,3 +112,48 @@ export interface RegenerateDevGuideResponse {
generationType: DevGuideGenerationType;
remainingRegenerationCount?: number | null;
}
+
+export interface ConfirmDevGuideRequest {
+ devGuideId: number;
+ projectGroupId: number;
+}
+
+export interface DevGuideHistory {
+ confirmed: boolean;
+ createdAt: string;
+ devGuideId: number;
+ generationType: DevGuideGenerationType;
+ versionNo: number;
+}
+
+export interface DevGuideHistoryListResponse {
+ histories: DevGuideHistory[];
+}
+
+export type DevGuideHistoryContentResponse = DevGuideContent & DevGuideHistory;
+
+export interface GithubWeeklySummaryRequest {
+ projectGroupId: number;
+ targetUserId: number;
+}
+
+export interface GithubWeeklySummaryContent {
+ followUpSuggestions: string[];
+ issueHighlights: string[];
+ mainActivities: string[];
+ pullRequestHighlights: string[];
+ summary: string;
+}
+
+export interface GithubWeeklySummary {
+ periodEnd: string;
+ periodStart: string;
+ sourceIssueCount: number;
+ sourcePrCount: number;
+ summary: GithubWeeklySummaryContent;
+ weeklyGithubSummaryId: number;
+}
+
+export interface GithubWeeklySummaryListResponse {
+ summaries: GithubWeeklySummary[];
+}