Skip to content

Commit

Permalink
feat: 簡易的なTODO管理Webアプリケーションを実装
Browse files Browse the repository at this point in the history
- Next.js 14 (App Router) + TypeScript + Prisma + Tailwind CSSで実装

- TODOの追加、一覧表示、完了/未完了の切り替え、削除機能を実装

- REST APIエンドポイント(GET, POST, PATCH, DELETE)を実装

- コンポーネントテストとAPIテストを実装

- SQLiteデータベースを使用(PostgreSQL等への移行が容易)

- Zodによる入力検証を実装
  • Loading branch information
User committed Jan 12, 2026
1 parent d7ff49b commit 0f32688
Show file tree
Hide file tree
Showing 25 changed files with 10,007 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
40 changes: 40 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# prisma
/prisma/*.db
/prisma/*.db-journal
98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,97 @@
# todo-review-test
# TODO管理アプリ

簡易的なTODO管理Webアプリケーションです。

## 技術スタック

- **フレームワーク**: Next.js 14 (App Router)
- **言語**: TypeScript
- **データベース**: SQLite (Prisma ORM)
- **スタイリング**: Tailwind CSS
- **テスト**: Vitest + Testing Library

## 機能

- TODOの追加
- TODOの一覧表示
- TODOの完了/未完了の切り替え
- TODOの削除
- 完了済みTODOと未完了TODOの分類表示

## セットアップ

### 1. 依存関係のインストール

```bash
npm install
```

### 2. 環境変数の設定

`.env`ファイルを作成し、以下の内容を追加してください:

```
DATABASE_URL="file:./prisma/dev.db"
```

### 3. データベースの初期化

```bash
npm run db:push
npm run db:generate
```

### 4. 開発サーバーの起動

```bash
npm run dev
```

ブラウザで [http://localhost:3000](http://localhost:3000) を開いてください。

## スクリプト

- `npm run dev` - 開発サーバーを起動
- `npm run build` - プロダクションビルド
- `npm run start` - プロダクションサーバーを起動
- `npm run lint` - ESLintでコードをチェック
- `npm run test` - テストを実行
- `npm run test:watch` - ウォッチモードでテストを実行
- `npm run db:push` - データベーススキーマをプッシュ
- `npm run db:generate` - Prismaクライアントを生成
- `npm run db:studio` - Prisma Studioを起動

## プロジェクト構造

```
todo-review-test/
├── app/
│ ├── api/
│ │ └── todos/ # TODO APIエンドポイント
│ ├── globals.css # グローバルスタイル
│ ├── layout.tsx # ルートレイアウト
│ └── page.tsx # ホームページ
├── components/
│ ├── TodoForm.tsx # TODO追加フォーム
│ ├── TodoList.tsx # TODO一覧
│ └── TodoItem.tsx # TODOアイテム
├── lib/
│ └── prisma.ts # Prismaクライアント
├── prisma/
│ └── schema.prisma # データベーススキーマ
└── tests/ # テストファイル
```

## 拡張性

このアプリケーションは以下の点で拡張しやすく設計されています:

- **型安全性**: TypeScriptとPrismaによる完全な型安全性
- **モジュール設計**: コンポーネントとAPIルートの分離
- **データベース**: SQLiteからPostgreSQLなどへの移行が容易
- **テスト**: 包括的なテストカバレッジ
- **バリデーション**: Zodによる入力検証

## ライセンス

MIT
101 changes: 101 additions & 0 deletions app/api/todos/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'

const updateTodoSchema = z.object({
title: z.string().min(1, 'タイトルは必須です').max(200, 'タイトルは200文字以内で入力してください').optional(),
description: z.string().max(1000, '説明は1000文字以内で入力してください').optional(),
completed: z.boolean().optional(),
})

// GET: 特定のTODOを取得
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const todo = await prisma.todo.findUnique({
where: { id },
})

if (!todo) {
return NextResponse.json(
{ error: 'TODOが見つかりません' },
{ status: 404 }
)
}

return NextResponse.json(todo)
} catch (error) {
console.error('Error fetching todo:', error)
return NextResponse.json(
{ error: 'TODOの取得に失敗しました' },
{ status: 500 }
)
}
}

// PATCH: TODOを更新
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const validatedData = updateTodoSchema.parse(body)

const todo = await prisma.todo.update({
where: { id },
data: validatedData,
})

return NextResponse.json(todo)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: error.errors[0].message },
{ status: 400 }
)
}
if (error instanceof Error && error.message.includes('Record to update not found')) {
return NextResponse.json(
{ error: 'TODOが見つかりません' },
{ status: 404 }
)
}
console.error('Error updating todo:', error)
return NextResponse.json(
{ error: 'TODOの更新に失敗しました' },
{ status: 500 }
)
}
}

// DELETE: TODOを削除
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
await prisma.todo.delete({
where: { id },
})

return NextResponse.json({ message: 'TODOを削除しました' })
} catch (error) {
if (error instanceof Error && error.message.includes('Record to delete does not exist')) {
return NextResponse.json(
{ error: 'TODOが見つかりません' },
{ status: 404 }
)
}
console.error('Error deleting todo:', error)
return NextResponse.json(
{ error: 'TODOの削除に失敗しました' },
{ status: 500 }
)
}
}
55 changes: 55 additions & 0 deletions app/api/todos/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'

const todoSchema = z.object({
title: z.string().min(1, 'タイトルは必須です').max(200, 'タイトルは200文字以内で入力してください'),
description: z.string().max(1000, '説明は1000文字以内で入力してください').optional(),
})

// GET: すべてのTODOを取得
export async function GET() {
try {
const todos = await prisma.todo.findMany({
orderBy: {
createdAt: 'desc',
},
})
return NextResponse.json(todos)
} catch (error) {
console.error('Error fetching todos:', error)
return NextResponse.json(
{ error: 'TODOの取得に失敗しました' },
{ status: 500 }
)
}
}

// POST: 新しいTODOを作成
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validatedData = todoSchema.parse(body)

const todo = await prisma.todo.create({
data: {
title: validatedData.title,
description: validatedData.description,
},
})

return NextResponse.json(todo, { status: 201 })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: error.errors[0].message },
{ status: 400 }
)
}
console.error('Error creating todo:', error)
return NextResponse.json(
{ error: 'TODOの作成に失敗しました' },
{ status: 500 }
)
}
}
27 changes: 27 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--background: #ffffff;
--foreground: #171717;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
19 changes: 19 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "TODO管理アプリ",
description: "簡易的なTODO管理Webアプリケーション",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja">
<body>{children}</body>
</html>
);
}
20 changes: 20 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import TodoList from '@/components/TodoList'
import TodoForm from '@/components/TodoForm'

export default function Home() {
return (
<main className="min-h-screen bg-gray-50 py-8">
<div className="max-w-2xl mx-auto px-4">
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">
TODO管理アプリ
</h1>
<div className="bg-white rounded-lg shadow-md p-6">
<TodoForm />
<div className="mt-8">
<TodoList />
</div>
</div>
</div>
</main>
)
}
Loading

0 comments on commit 0f32688

Please sign in to comment.