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
41 changes: 41 additions & 0 deletions backend/alembic_migrations/versions/0039_add_capital_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""resume_experiences に capital_unit カラムを追加する

資本金の単位(万円 / 百万円 / 千万円 / 億円)を在籍企業ごとに選択できるようにする。
従来は表示時に「千万円」固定だったため、既存行の後方互換として server_default を
「千万円」にする。

libSQL (SQLite 互換) は ALTER COLUMN / DROP COLUMN を直接サポートしないため、
downgrade の列削除は batch_alter_table(テーブル再作成)で行う。ADD COLUMN は
SQLite が直接サポートするため upgrade は op.add_column をそのまま使う。

Revision ID: 0039_add_capital_unit
Revises: 0038_add_project_periods_table
Create Date: 2026-05-29 00:00:00.000000
"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

revision: str = "0039_add_capital_unit"
down_revision: Union[str, None] = "0038_add_project_periods_table"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"resume_experiences",
sa.Column(
"capital_unit",
sa.String(12),
nullable=False,
server_default="千万円",
),
)


def downgrade() -> None:
with op.batch_alter_table("resume_experiences") as batch_op:
batch_op.drop_column("capital_unit")
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""非IT経歴フラグ・詳細と休暇エントリのカラムを追加する

- resume_experiences に is_it_company(IT企業かどうか)と description(非IT時の詳細)を追加。
既存行は IT 企業前提だったため is_it_company の server_default を True(1) にする。
- resume_clients に is_vacation(休暇エントリか)と vacation_* 期間/詳細を追加。
既存行は通常の取引先のため is_vacation の server_default を False(0) にする。

libSQL (SQLite 互換) は ADD COLUMN を直接サポートするため upgrade は op.add_column を
そのまま使う。downgrade の列削除は ALTER/DROP COLUMN 非対応のため batch_alter_table
(テーブル再作成)で行う。

Revision ID: 0040_add_resume_non_it_and_vacation
Revises: 0039_add_capital_unit
Create Date: 2026-05-29 00:00:00.000000
"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

revision: str = "0040_add_resume_non_it_and_vacation"
down_revision: Union[str, None] = "0039_add_capital_unit"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"resume_experiences",
sa.Column("is_it_company", sa.Boolean(), nullable=False, server_default=sa.text("1")),
)
op.add_column(
"resume_experiences",
sa.Column("description", sa.Text(), nullable=False, server_default=""),
)
op.add_column(
"resume_clients",
sa.Column("is_vacation", sa.Boolean(), nullable=False, server_default=sa.text("0")),
)
op.add_column(
"resume_clients",
sa.Column("vacation_start_date", sa.Date(), nullable=True),
)
op.add_column(
"resume_clients",
sa.Column("vacation_end_date", sa.Date(), nullable=True),
)
op.add_column(
"resume_clients",
sa.Column("vacation_is_current", sa.Boolean(), nullable=False, server_default=sa.text("0")),
)
op.add_column(
"resume_clients",
sa.Column("vacation_description", sa.Text(), nullable=False, server_default=""),
)


def downgrade() -> None:
with op.batch_alter_table("resume_clients") as batch_op:
batch_op.drop_column("vacation_description")
batch_op.drop_column("vacation_is_current")
batch_op.drop_column("vacation_end_date")
batch_op.drop_column("vacation_start_date")
batch_op.drop_column("is_vacation")
with op.batch_alter_table("resume_experiences") as batch_op:
batch_op.drop_column("description")
batch_op.drop_column("is_it_company")
3 changes: 2 additions & 1 deletion backend/app/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"github_user_not_found": "GitHubユーザーが見つかりません: {username}",
"profile_fetch_failed": "GitHubプロフィールの取得に失敗しました。しばらくしてから再度お試しください。",
"no_link_cache": "連携データがありません。先にGitHub連携を実行してください。",
"not_retryable": "このタスクはリトライできない状態です(現在: {status})。"
"not_retryable": "このタスクはリトライできない状態です(現在: {status})。",
"contribution_fetch_failed": "コントリビューション情報の取得に失敗しました。スキル集計は表示されています。"
},
"validation": {
"required_field": "{field}は必須です。",
Expand Down
25 changes: 25 additions & 0 deletions backend/app/models/resume.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ class ResumeExperience(Base):
is_current: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
employee_count: Mapped[str] = mapped_column(String(60), nullable=False, default="")
capital: Mapped[str] = mapped_column(String(120), nullable=False, default="")
# 資本金の単位(万円 / 百万円 / 千万円 / 億円)。既定は後方互換のため「千万円」。
capital_unit: Mapped[str] = mapped_column(String(12), nullable=False, default="千万円")
# IT 企業かどうか。False(非IT)の場合は取引先/プロジェクトを持たず description を使う。
is_it_company: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
# 非IT企業の職務内容を記述する自由記述欄(見出し「詳細」)。
description: Mapped[str] = mapped_column(Text, nullable=False, default="")
client_rows: Mapped[list["ResumeClient"]] = relationship(
back_populates="experience",
cascade="all, delete-orphan",
Expand Down Expand Up @@ -138,6 +144,16 @@ class ResumeClient(Base):
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
name: Mapped[str] = mapped_column(String(200), nullable=False, default="")
has_client: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
# 休暇エントリ。True のとき取引先ではなく在籍中の休暇を表し vacation_* を使う。
is_vacation: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
vacation_start_date_value: Mapped[date | None] = mapped_column(
"vacation_start_date", Date, nullable=True
)
vacation_end_date_value: Mapped[date | None] = mapped_column(
"vacation_end_date", Date, nullable=True
)
vacation_is_current: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
vacation_description: Mapped[str] = mapped_column(Text, nullable=False, default="")
project_rows: Mapped[list["ResumeProject"]] = relationship(
back_populates="client",
cascade="all, delete-orphan",
Expand All @@ -150,6 +166,15 @@ def projects(self) -> list["ResumeProject"]:
"""プロジェクトを期間の降順でソートして返す。"""
return sort_by_period_desc(list(self.project_rows))

@property
def vacation_start_date(self) -> str:
return format_year_month(self.vacation_start_date_value) or ""

@property
def vacation_end_date(self) -> str:
"""DB の vacation_end_date が NULL(継続中)の場合は "" を返す。"""
return format_year_month(self.vacation_end_date_value) or ""


class ResumeProject(Base):
__tablename__ = "resume_projects"
Expand Down
10 changes: 10 additions & 0 deletions backend/app/repositories/resume.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def _build_experience_row(self, index: int, payload: dict[str, object]) -> Resum
is_current=payload.get("is_current", False),
employee_count=payload.get("employee_count", ""),
capital=payload.get("capital", ""),
capital_unit=payload.get("capital_unit", "千万円"),
is_it_company=payload.get("is_it_company", True),
description=payload.get("description", ""),
client_rows=[
self._build_client_row(client_index, client)
for client_index, client in enumerate(payload.get("clients", []))
Expand All @@ -86,10 +89,17 @@ def _build_experience_row(self, index: int, payload: dict[str, object]) -> Resum
def _build_client_row(self, index: int, payload: dict[str, object]) -> ResumeClient:
projects = list(payload.get("projects", []))
sorted_projects = sorted(projects, key=self._project_sort_key)
vacation_start = payload.get("vacation_start_date") or ""
vacation_end = payload.get("vacation_end_date") or ""
return ResumeClient(
sort_order=index,
name=payload.get("name", ""),
has_client=payload.get("has_client", True),
is_vacation=payload.get("is_vacation", False),
vacation_start_date_value=parse_year_month(vacation_start) if vacation_start else None,
vacation_end_date_value=parse_year_month(vacation_end) if vacation_end else None,
vacation_is_current=payload.get("vacation_is_current", False),
vacation_description=payload.get("vacation_description", ""),
project_rows=[
self._build_project_row(project_index, project)
for project_index, project in enumerate(sorted_projects)
Expand Down
22 changes: 22 additions & 0 deletions backend/app/schemas/github_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ class GitHubLinkRequest(BaseModel):
)


class ContributionDay(BaseModel):
"""コントリビューションカレンダーの 1 日分。"""

date: str = Field(description="ISO 8601 形式の日付 (YYYY-MM-DD)")
count: int = Field(description="その日のコントリビューション数")
level: int = Field(description="GitHub の濃淡レベル (0–4)")


class ContributionCalendar(BaseModel):
"""直近1年のコントリビューションカレンダー(GitHub の緑の四角)。"""

total_contributions: int = Field(description="期間内のコントリビューション総数")
weeks: list[list[ContributionDay]] = Field(
default_factory=list,
description="週ごとの日配列(列=週、各週は最大7日)",
)


class GitHubLinkResponse(BaseModel):
username: str
repos_analyzed: int
Expand All @@ -36,6 +54,10 @@ class GitHubLinkResponse(BaseModel):
default_factory=dict,
description="ルートファイルから検出したインフラツール名 → 使用リポジトリ数",
)
contribution_calendar: Optional[ContributionCalendar] = Field(
default=None,
description="直近1年のコントリビューションカレンダー(取得失敗時は None)",
)


class CachedGitHubLinkResponse(BaseModel):
Expand Down
44 changes: 43 additions & 1 deletion backend/app/schemas/resume.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
``CareerProject`` / ``CareerClient`` / ``CareerExperience`` 等)と対になる DTO。
言語境界のため codegen 未導入の手動同期で運用している(エラーコードの errors.py と同方針)。
フィールドを増減・rename する場合は対応する FE type も同時に更新すること。

非IT経歴: ``Experience.is_it_company=False`` のとき取引先/プロジェクトを持たず
``Experience.description``(詳細)のみ使う。
休暇: ``Client.is_vacation=True`` のとき取引先ではなく在籍中の休暇を表し、
``Client.vacation_*``(期間・詳細)を使う。
"""

from datetime import datetime
Expand Down Expand Up @@ -125,14 +130,45 @@ def _migrate_legacy_fields(cls, data: dict) -> dict:
return data

class Client(BaseModel):
"""ユーザ(常駐先/クライアント企業)。"""
"""ユーザ(常駐先/クライアント企業)。

``is_vacation=True`` の場合は取引先ではなく在籍中の休暇(育児/介護/留学等)を表し、
name / projects の代わりに ``vacation_*`` 期間と詳細を保持する。
"""

name: str = Field(max_length=200, default="")
has_client: bool = True
projects: list[Project] = Field(default_factory=list)
# 休暇エントリ。True のとき vacation_* を期間・詳細として扱い projects は無視する。
is_vacation: bool = False
vacation_start_date: str = Field(default="", max_length=30)
# 継続中(vacation_is_current=True)の場合は "" を渡す契約。
vacation_end_date: str = Field(default="", max_length=30)
vacation_is_current: bool = False
vacation_description: str = Field(max_length=4500, default="")

model_config = ConfigDict(from_attributes=True)

@model_validator(mode="after")
def validate_vacation_dates(self) -> "Client":
"""休暇の期間を検証する(Experience.validate_dates と同じ契約)。

休暇でない取引先は検証対象外。開始年月は必須、継続中なら end を ""
に正規化し、それ以外は終了年月必須かつ end >= start を要求する。
"""
if not self.is_vacation:
return self
if not self.vacation_start_date.strip():
raise ValueError(get_error("validation.start_date_required"))
if self.vacation_is_current:
self.vacation_end_date = ""
return self
if not self.vacation_end_date.strip():
raise ValueError(get_error("validation.end_date_required"))
if self.vacation_end_date < self.vacation_start_date:
raise ValueError(get_error("validation.date_range_invalid"))
return self


class Experience(BaseModel):
company: str = Field(min_length=1, max_length=120)
Expand All @@ -145,6 +181,12 @@ class Experience(BaseModel):
is_current: bool = False
employee_count: str = Field(max_length=60, default="")
capital: str = Field(max_length=120, default="")
# 資本金の単位。既定は後方互換のため「千万円」。FE の CapitalUnit と対の DTO。
capital_unit: Literal["万円", "百万円", "千万円", "億円"] = Field(default="千万円")
# IT 企業かどうか。False(非IT)の場合は取引先/プロジェクトを持たず description を使う。
is_it_company: bool = True
# 非IT企業の職務内容を記述する自由記述欄(見出し「詳細」)。business_description=事業内容 とは別。
description: str = Field(max_length=4500, default="")
clients: list[Client] = Field(default_factory=list)

model_config = ConfigDict(from_attributes=True)
Expand Down
Loading
Loading