Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions backend/app/schemas/github_link.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""GitHub 連携 API 用の Pydantic スキーマ。"""

from typing import Any, Dict, Optional
from typing import Dict, Optional

from pydantic import BaseModel, Field

Expand Down Expand Up @@ -63,7 +63,9 @@ class GitHubLinkResponse(BaseModel):
class CachedGitHubLinkResponse(BaseModel):
"""DB に保存された連携結果を返す。"""

result: Optional[Dict[str, Any]] = None
# cache.result は GitHubLinkResponse(...).model_dump() を保存したものなので、
# 型を絞って OpenAPI に GitHubLinkResponse / Contribution* を出力させる(ADR-0007 Phase 2)。
result: Optional[GitHubLinkResponse] = None
status: Optional[str] = None
error_message: Optional[str] = None
error_code: Optional[str] = None
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0007-openapi-typescript-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ FastAPI が出力する OpenAPI スキーマから **`openapi-typescript`** で
|---|---|---|---|
| 0 ✅完了 | 前提・基盤 | ①`response_model` 棚卸し(未設定 14 個を「DTO 不要」と「schema 化必要」に仕分け)②不足 schema 追加 + `response_model` 付与(`/github/login-url`→`GitHubLoginUrlResponse`、202 系→共通 `TaskAcceptedResponse`)③`backend/scripts/export_openapi.py` 追加 ④`openapi-typescript` を devDependency 追加 ⑤`make codegen-types`(Nix wrap)⑥`generated.ts` 初回生成 ⑦CI ドリフト検知(`git diff --exit-code`)| 中 |
| 1 ✅完了 | 読み取り(パイロット) | `api/shared.ts`(`TaskStatusResponse`)を完全移行。手書き interface を削除し、生成物の薄い再エクスポート層 `api/types.ts` へ統合。論点B(`str \| None`→`string \| null`)に伴い消費側 `useTaskPolling` / `useAsyncTaskPage` の `checkStatus` 契約を `string \| null` 許容へ更新。`make ci` green と「BE schema をわざと変えると `git diff --exit-code` が fail する」ことを確認済み | 低〜中 |
| 2 | 主要レスポンス | `api/githubLink.ts`(`TaskProgress`→`ProgressResponse` 含む)・`api/auth.ts`(`AuthResponse`→`TokenResponse`)を完全移行。手書き interface を全削除し BE 名へ rename。E2E 必須 | 中 |
| 2 ✅完了 | 主要レスポンス | `api/githubLink.ts`(`TaskProgress`→`ProgressResponse`、`{status}`→`TaskAcceptedResponse`、`GitHubLinkResponse` / `Contribution*` / `CachedGitHubLinkResponse`)・`api/auth.ts`(`AuthResponse`→`TokenResponse`、login-url→`GitHubLoginUrlResponse`)を完全移行。`CachedGitHubLinkResponse.result` を backend で `GitHubLinkResponse \| None` に絞り OpenAPI へ出力(型安全の後退を防止)。論点B(生成型 optional 化)に伴い `useAsyncTaskPage` の loadCache / `ContributionHeatmap` の weeks を null 合体で追従。`make ci` green・backend 68 passed・E2E 22 passed・codegen 冪等を確認済み。request 型 `GitHubLinkPayload`(default 値あり)は論点A により手書き温存 | 中 |
| 3 | フォーム入出力含む | `types.ts` の `CareerResume*`(→`Resume*`)/ `BlogAccount`(→`BlogAccountResponse`)/ `MasterItem` 系を完全移行。`payloadBuilders.ts` / `formMappers.ts` の追従。E2E 必須 | 中〜高 |

> **完全移行の前提(Phase 0 に内包)**: 生成できるのは `response_model` 付きエンドポイントのみ(実測 40 中 26)。DTO を持つのに `response_model` 未設定のエンドポイント(`/github/login-url` の `{authorization_url, state}`、`/run`・`/run/retry` の `{status}`)を schema 化・付与する作業を、完全移行の前提として Phase 0 で実施する。これを怠ると「生成物と手書きが混在する中途半端な状態」になり完全移行が成立しない。
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { FALLBACK_MESSAGES } from "../constants/messages";
import { API_BASE_URL, request } from "./client";
import { PATHS } from "./paths";
import type { GitHubLoginUrlResponse, TokenResponse } from "./types";

type AuthResponse = { username: string; is_github_user: boolean };

export async function getCurrentUser(): Promise<AuthResponse | null> {
export async function getCurrentUser(): Promise<TokenResponse | null> {
const response = await fetch(`${API_BASE_URL}${PATHS.auth.me}`, {
credentials: "include",
});
Expand All @@ -14,11 +13,11 @@ export async function getCurrentUser(): Promise<AuthResponse | null> {
if (!response.ok) {
throw new Error(FALLBACK_MESSAGES.AUTH_CHECK);
}
return (await response.json()) as AuthResponse;
return (await response.json()) as TokenResponse;
}

export async function handleGitHubCallback(code: string, state: string): Promise<AuthResponse> {
return request<AuthResponse>(PATHS.auth.githubCallback, {
export async function handleGitHubCallback(code: string, state: string): Promise<TokenResponse> {
return request<TokenResponse>(PATHS.auth.githubCallback, {
method: "POST",
body: JSON.stringify({ code, state }),
});
Expand All @@ -36,7 +35,7 @@ export async function initiateGitHubLogin(returnTo: string): Promise<void> {
credentials: "include",
});
if (!response.ok) throw new Error(FALLBACK_MESSAGES.GITHUB_OAUTH_START);
const data = (await response.json()) as { authorization_url: string; state: string };
const data = (await response.json()) as GitHubLoginUrlResponse;
// CSRF 検証用に state を sessionStorage へ保存する(コールバックで照合)
sessionStorage.setItem(GITHUB_OAUTH_STATE_STORAGE_KEY, data.state);
window.location.assign(data.authorization_url);
Expand Down
81 changes: 79 additions & 2 deletions frontend/src/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,8 +809,7 @@ export interface components {
error_code?: string | null;
/** Error Message */
error_message?: string | null;
/** Result */
result?: Record<string, never> | null;
result?: components["schemas"]["GitHubLinkResponse"] | null;
/** Status */
status?: string | null;
/** Warning Message */
Expand Down Expand Up @@ -908,6 +907,43 @@ export interface components {
*/
vacation_start_date: string;
};
/**
* ContributionCalendar
* @description 直近1年のコントリビューションカレンダー(GitHub の緑の四角)。
*/
ContributionCalendar: {
/**
* Total Contributions
* @description 期間内のコントリビューション総数
*/
total_contributions: number;
/**
* Weeks
* @description 週ごとの日配列(列=週、各週は最大7日)
*/
weeks?: components["schemas"]["ContributionDay"][][];
};
/**
* ContributionDay
* @description コントリビューションカレンダーの 1 日分。
*/
ContributionDay: {
/**
* Count
* @description その日のコントリビューション数
*/
count: number;
/**
* Date
* @description ISO 8601 形式の日付 (YYYY-MM-DD)
*/
date: string;
/**
* Level
* @description GitHub の濃淡レベル (0–4)
*/
level: number;
};
/** Experience */
"Experience-Input": {
/** Business Description */
Expand Down Expand Up @@ -1024,6 +1060,47 @@ export interface components {
*/
include_forks: boolean;
};
/** GitHubLinkResponse */
GitHubLinkResponse: {
/** Analyzed At */
analyzed_at: string;
/** @description 直近1年のコントリビューションカレンダー(取得失敗時は None) */
contribution_calendar?: components["schemas"]["ContributionCalendar"] | null;
/**
* Detected Devtools
* @description ルートファイルから検出した DevTools 名 → 使用リポジトリ数
*/
detected_devtools?: {
[key: string]: number;
};
/**
* Detected Frameworks
* @description 依存関係から検出したフレームワーク名 → 使用リポジトリ数
*/
detected_frameworks?: {
[key: string]: number;
};
/**
* Detected Infras
* @description ルートファイルから検出したインフラツール名 → 使用リポジトリ数
*/
detected_infras?: {
[key: string]: number;
};
/**
* Languages
* @description 言語ごとのバイト数(GitHub linguist ベース)
*/
languages?: {
[key: string]: number;
};
/** Repos Analyzed */
repos_analyzed: number;
/** Unique Skills */
unique_skills: number;
/** Username */
username: string;
};
/**
* GitHubLoginUrlResponse
* @description GitHub OAuth 認可 URL と CSRF 検証用 state を返すレスポンス。
Expand Down
75 changes: 17 additions & 58 deletions frontend/src/api/githubLink.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,26 @@
import { request } from "./client";
import { PATHS } from "./paths";
import type { TaskStatusResponse } from "./types";

export interface TaskProgress {
task_id: string;
step_index: number;
total_steps: number;
step_label: string | null;
sub_progress: { done: number; total: number } | null;
}
import type {
CachedGitHubLinkResponse,
ProgressResponse,
TaskAcceptedResponse,
TaskStatusResponse,
} from "./types";

/**
* GitHub 連携の開始リクエスト body。
* backend `schemas/github_link.py:GitHubLinkRequest` 相当だが、呼び出し側で
* 省略可能にするため include_forks を optional にした手書きの request 型(ADR-0007 論点A)。
*/
export interface GitHubLinkPayload {
include_forks?: boolean;
}

/** コントリビューションカレンダーの 1 日分 */
export interface ContributionDay {
/** ISO 8601 形式の日付 (YYYY-MM-DD) */
date: string;
/** その日のコントリビューション数 */
count: number;
/** GitHub の濃淡レベル (0–4) */
level: number;
}

/** 直近1年のコントリビューションカレンダー(GitHub の緑の四角) */
export interface ContributionCalendar {
/** 期間内のコントリビューション総数 */
total_contributions: number;
/** 週ごとの日配列(列=週、各週は最大7日) */
weeks: ContributionDay[][];
}

export interface GitHubLinkResponse {
username: string;
repos_analyzed: number;
unique_skills: number;
analyzed_at: string;
languages: Record<string, number>;
/** 依存関係から検出したフレームワーク名 → 使用リポジトリ数 */
detected_frameworks: Record<string, number>;
/** ルートファイルから検出した DevTools 名 → 使用リポジトリ数 */
detected_devtools: Record<string, number>;
/** ルートファイルから検出したインフラツール名 → 使用リポジトリ数 */
detected_infras: Record<string, number>;
/** 直近1年のコントリビューションカレンダー(取得失敗時は null) */
contribution_calendar?: ContributionCalendar | null;
}

export interface CachedGitHubLinkResponse {
result: GitHubLinkResponse | null;
status?: string;
error_message?: string;
error_code?: string;
/** 連携自体は完了したが部分的に欠落した場合の警告 */
warning_message?: string;
}

/**
* GitHub 連携を開始します(202 非同期)。
*/
export function runGitHubLink(payload: GitHubLinkPayload): Promise<{ status: string }> {
return request<{ status: string }>(PATHS.githubLink.run, {
export function runGitHubLink(payload: GitHubLinkPayload): Promise<TaskAcceptedResponse> {
return request<TaskAcceptedResponse>(PATHS.githubLink.run, {
method: "POST",
body: JSON.stringify(payload),
});
Expand All @@ -85,17 +44,17 @@ export function getGitHubLinkCacheStatus(): Promise<TaskStatusResponse> {
* GitHub 連携タスクの進捗を取得します。
* Redis が利用できない場合は step_index=0 のデフォルト値が返ります。
*/
export function getGitHubLinkProgress(): Promise<TaskProgress> {
return request<TaskProgress>(PATHS.githubLink.progress);
export function getGitHubLinkProgress(): Promise<ProgressResponse> {
return request<ProgressResponse>(PATHS.githubLink.progress);
}

/**
* 失敗した GitHub 連携タスクを手動で再実行します(202 非同期)。
*/
export function retryGitHubLink(
payload: GitHubLinkPayload = {},
): Promise<{ status: string }> {
return request<{ status: string }>(PATHS.githubLink.runRetry, {
): Promise<TaskAcceptedResponse> {
return request<TaskAcceptedResponse>(PATHS.githubLink.runRetry, {
method: "POST",
body: JSON.stringify(payload),
});
Expand Down
31 changes: 30 additions & 1 deletion frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,37 @@
*/
import type { components } from "./generated";

type Schemas = components["schemas"];

/**
* 非同期タスクのステータスを返す軽量レスポンス。
* backend `app/schemas/shared.py:TaskStatusResponse` のミラー。
*/
export type TaskStatusResponse = components["schemas"]["TaskStatusResponse"];
export type TaskStatusResponse = Schemas["TaskStatusResponse"];

/** 非同期タスクの受付応答(202 Accepted)。backend `schemas/shared.py:TaskAcceptedResponse`。 */
export type TaskAcceptedResponse = Schemas["TaskAcceptedResponse"];

/** GitHub 連携タスクの進捗。backend `schemas/shared.py:ProgressResponse`。 */
export type ProgressResponse = Schemas["ProgressResponse"];

/** 進捗の細粒度サブ進捗。backend `schemas/shared.py:SubProgress`。 */
export type SubProgress = Schemas["SubProgress"];

/** コントリビューションカレンダーの 1 日分。backend `schemas/github_link.py:ContributionDay`。 */
export type ContributionDay = Schemas["ContributionDay"];

/** 直近1年のコントリビューションカレンダー。backend `schemas/github_link.py:ContributionCalendar`。 */
export type ContributionCalendar = Schemas["ContributionCalendar"];

/** GitHub 連携の解析結果。backend `schemas/github_link.py:GitHubLinkResponse`。 */
export type GitHubLinkResponse = Schemas["GitHubLinkResponse"];

/** DB に保存された連携結果。backend `schemas/github_link.py:CachedGitHubLinkResponse`。 */
export type CachedGitHubLinkResponse = Schemas["CachedGitHubLinkResponse"];

/** GitHub OAuth 認可 URL と CSRF 検証用 state。backend `schemas/auth.py:GitHubLoginUrlResponse`。 */
export type GitHubLoginUrlResponse = Schemas["GitHubLoginUrlResponse"];

/** 認証トークン応答(ログインユーザー情報)。backend `schemas/auth.py:TokenResponse`。 */
export type TokenResponse = Schemas["TokenResponse"];
15 changes: 9 additions & 6 deletions frontend/src/components/github-link/ContributionHeatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,28 @@ interface ContributionHeatmapProps {
* 年間コントリビュート数と最大連続日数のサマリーも表示する。
*/
export function ContributionHeatmap({ calendar }: ContributionHeatmapProps) {
// weeks は OpenAPI 生成型では optional(backend の default_factory 由来)。null 合体で安定参照にする。
const weeks = useMemo(() => calendar.weeks ?? [], [calendar.weeks]);

/** 各週の先頭日から月ラベルを算出する(前の週から月が変わる週にのみ表示) */
const monthLabels = useMemo(() => {
// "YYYY-MM-DD" は UTC 深夜として parse されるため、ローカル TZ で月がずれない
// よう getUTCMonth() で月を取り出す(負オフセット TZ での月境界ずれを防ぐ)
const monthOf = (week: ContributionDay[]) =>
week[0] ? new Date(week[0].date).getUTCMonth() : -1;
return calendar.weeks.map((week, i) => {
return weeks.map((week, i) => {
const month = monthOf(week);
if (month < 0) return "";
const prevMonth = i > 0 ? monthOf(calendar.weeks[i - 1]) : -1;
const prevMonth = i > 0 ? monthOf(weeks[i - 1]) : -1;
return month !== prevMonth ? MONTH_NAMES[month] : "";
});
}, [calendar.weeks]);
}, [weeks]);

/** 連続コントリビュート日数の最大値を算出する */
const longestStreak = useMemo(() => {
let longest = 0;
let current = 0;
for (const day of calendar.weeks.flat()) {
for (const day of weeks.flat()) {
if (day.count > 0) {
current += 1;
longest = Math.max(longest, current);
Expand All @@ -46,7 +49,7 @@ export function ContributionHeatmap({ calendar }: ContributionHeatmapProps) {
}
}
return longest;
}, [calendar.weeks]);
}, [weeks]);

return (
<div className={styles.container}>
Expand Down Expand Up @@ -74,7 +77,7 @@ export function ContributionHeatmap({ calendar }: ContributionHeatmapProps) {
))}
</div>
<div className={styles.grid}>
{calendar.weeks.map((week, wi) => (
{weeks.map((week, wi) => (
<div key={wi} className={styles.weekCol}>
{week.map((day) => (
<span
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/github-link/GitHubLinkDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export function GitHubLinkDashboard() {
useAsyncTaskPage<GitHubLinkResponse>({
loadCache: async () => {
const cache = await getGitHubLinkCache();
return { result: cache.result, status: cache.status };
// result は生成型では optional(`GitHubLinkResponse | null | undefined`)のため null に正規化する。
return { result: cache.result ?? null, status: cache.status };
},
checkStatus: getGitHubLinkCacheStatus,
fetchProgress: getGitHubLinkProgress,
Expand Down
Loading
Loading