-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- 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
Showing
25 changed files
with
10,007 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": "next/core-web-vitals" | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } | ||
| ) | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } | ||
| ) | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ); | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
Oops, something went wrong.