Skip to content
Merged

a #279

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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import sqlalchemy as sa
from alembic import op
from sqlalchemy import text

# revision identifiers, used by Alembic.
revision: str = "0037_merge_project_caf_into_description"
Expand All @@ -27,6 +28,20 @@


def upgrade() -> None:
conn = op.get_bind()
result = conn.execute(
text(
"SELECT COUNT(*) FROM resume_projects"
" WHERE challenge != '' OR action != '' OR result != ''"
)
)
count = result.scalar() or 0
if count > 0:
raise RuntimeError(
f"resume_projects に challenge/action/result のデータが {count} 件残っています。"
"カラム削除前にデータを退避してください。"
)

with op.batch_alter_table("resume_projects") as batch_op:
batch_op.add_column(
sa.Column("description", sa.Text(), nullable=False, server_default=""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ export function CareerExperienceEditor({
<label>
{/* グローバル CSS で label { display: grid } のため、テキストと DirtyDot を span で
束ねないと別々の行になる。span で 1 グリッド行に束ねることでラベル右側に並べる。 */}
<span>
<span className={shared.labelText}>
会社名
<span className={shared.requiredBadge}>必須</span>
<DirtyDot visible={Boolean(fieldDirty?.company)} />
</span>
<input
Expand All @@ -92,8 +93,9 @@ export function CareerExperienceEditor({
/>
</label>
<label>
<span>
<span className={shared.labelText}>
事業内容
<span className={shared.requiredBadge}>必須</span>
<DirtyDot visible={Boolean(fieldDirty?.business_description)} />
</span>
<input
Expand All @@ -109,8 +111,9 @@ export function CareerExperienceEditor({

<div className={shared.inline}>
<label>
<span>
<span className={shared.labelText}>
開始
<span className={shared.requiredBadge}>必須</span>
<DirtyDot visible={Boolean(fieldDirty?.start_date)} />
</span>
<input
Expand All @@ -136,8 +139,9 @@ export function CareerExperienceEditor({
</label>
{!exp.is_current && (
<label>
<span>
<span className={shared.labelText}>
離職年月
<span className={shared.requiredBadge}>必須</span>
<DirtyDot visible={Boolean(fieldDirty?.end_date)} />
</span>
<input
Expand All @@ -164,6 +168,7 @@ export function CareerExperienceEditor({
<input
type="number"
min="0"
step="1"
value={exp.employee_count}
onChange={(e) =>
onUpdateExperienceField(expIndex, "employee_count", e.target.value)
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/forms/ProjectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Combobox } from "./Combobox";
import { MarkdownTextarea } from "./MarkdownTextarea";
import { ResumePdfTracePanel } from "./ResumePdfTracePanel";
import { DirtyDot } from "../ui/DirtyDot";
import shared from "../../styles/shared.module.css";
import styles from "./ProjectModal.module.css";

type ProjectModalProps = {
Expand Down Expand Up @@ -126,7 +127,10 @@ export function ProjectModal({
{local.periods.map((period, periodIndex) => (
<div key={`period-${periodIndex}`} className={styles.inline}>
<label>
<span>開始</span>
<span className={shared.labelText}>
開始
<span className={shared.requiredBadge}>必須</span>
</span>
<input
type="month"
value={period.start_date}
Expand All @@ -147,7 +151,10 @@ export function ProjectModal({
</label>
{!period.is_current && (
<label>
<span>終了</span>
<span className={shared.labelText}>
終了
<span className={shared.requiredBadge}>必須</span>
</span>
<input
type="month"
value={period.end_date}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ export function CareerQualificationsSection({
<div key={`qualification-${index}`} className={shared.entry}>
<div className={shared.inline}>
<label>
<span>
資格名 ※プルダウンにないものはテキストで入力できます。
<span className={shared.labelText}>
資格名
<span className={shared.requiredBadge}>必須</span>
※プルダウンにないものはテキストで入力できます。
<DirtyDot visible={Boolean(rowDirty?.fields.name)} />
</span>
<Combobox
Expand All @@ -102,8 +104,9 @@ export function CareerQualificationsSection({
/>
</label>
<label>
<span>
<span className={shared.labelText}>
取得日
<span className={shared.requiredBadge}>必須</span>
<DirtyDot visible={Boolean(rowDirty?.fields.acquired_date)} />
</span>
<input
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/hooks/career/usePdfPanelLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export function usePdfPanelLayout(
return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
document.body.style.userSelect = "";
document.body.style.cursor = "";
};
}, [containerRef, minWidth, minFormWidth, reservedGap]);

Expand Down
18 changes: 18 additions & 0 deletions frontend/src/hooks/career/useProjectFormDirty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,22 @@ describe("useProjectFormDirty", () => {
const { result } = renderHook(() => useProjectFormDirty(local, null));
expect(result.current.any).toBe(false);
});

it("description を変更すると fields.description と any が true", () => {
const original = buildProject();
const local = buildProject({ description: "変更後の詳細" });
const { result } = renderHook(() => useProjectFormDirty(local, original));
expect(result.current.fields.description).toBe(true);
expect(result.current.any).toBe(true);
expect(result.current.fields.name).toBe(false);
expect(result.current.team).toBe(false);
});

it("description を元の値に戻すと fields.description と any が false", () => {
const original = buildProject();
const local = buildProject({ description: original.description });
const { result } = renderHook(() => useProjectFormDirty(local, original));
expect(result.current.fields.description).toBe(false);
expect(result.current.any).toBe(false);
});
});
41 changes: 41 additions & 0 deletions frontend/src/payloadBuilders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,26 @@ describe("buildCareerPayload (projects/clients/team)", () => {
).toThrow(/プロジェクトの開始年月/);
});

it("内容のある project に period が 1 件も無いならエラー", () => {
expect(() =>
buildCareerPayload(
baseState({
experiences: [
blankExperience({
clients: [
{
name: "顧客A",
has_client: true,
projects: [blankProject({ name: "P", description: "詳細", periods: [] })],
},
],
}),
],
}),
),
).toThrow(/プロジェクトの開始年月/);
});

it("project の period が is_current=false で終了年月が空ならエラー", () => {
expect(() =>
buildCareerPayload(
Expand Down Expand Up @@ -381,6 +401,27 @@ describe("buildCareerPayload (projects/clients/team)", () => {
{ category: "db", name: "PostgreSQL" },
]);
});

it("project の name が空で description に内容があればプロジェクトはペイロードに含まれる", () => {
const payload = buildCareerPayload(
baseState({
experiences: [
blankExperience({
clients: [
{
name: "C",
has_client: true,
projects: [blankProject({ name: "", description: "開発の詳細" })],
},
],
}),
],
}),
);
const project = payload.experiences[0].clients[0].projects[0];
expect(project.name).toBe("");
expect(project.description).toBe("開発の詳細");
});
});

// ── qualifications の境界 ────────────────────────────────────
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/payloadBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ export function buildCareerPayload(state: CareerFormState): CareerResumePayload
for (const client of exp.clients) {
for (const proj of client.projects) {
// 内容のあるプロジェクトは periods が 1 件以上あり、各期間の開始年月が必須。
if (proj.periods.length === 0) {
throw new Error(VALIDATION_MESSAGES.PROJECT_START_DATE_REQUIRED);
}
for (const period of proj.periods) {
if (!period.start_date) {
throw new Error(VALIDATION_MESSAGES.PROJECT_START_DATE_REQUIRED);
Expand Down
Loading