Todo 앱을 React + Tailwind + Node.js + Express + SQLite로 구현하는 스킬. 투두 CRUD, 상세항목(textarea), 텍스트 그룹, 즐겨찾기, 상단고정, 탭 화면, 검색 하이라이트, 소프트 딜리트, 확인모달, 그룹별 그룹핑, 백업 기능을 포함한다. 'todo 만들어줘', 'todo 앱 구현', 'todo 페이지 작성' 요청 시 반드시 이 스킬을 사용할 것.
React + Tailwind CSS 프론트엔드와 Node.js + Express + SQLite 백엔드로 구성된 Todo 앱. 아래 파일 목록의 코드를 있는 그대로 생성하라. 임의로 변경하지 말 것.
todo/
├── server/
│ ├── package.json
│ ├── index.js
│ ├── db.js
│ └── routes/
│ ├── todos.js
│ ├── details.js
│ ├── groups.js
│ └── backup.js
├── client/
│ ├── package.json
│ ├── vite.config.js
│ ├── tailwind.config.js
│ ├── postcss.config.js
│ ├── index.html
│ └── src/
│ ├── index.jsx
│ ├── index.css
│ ├── App.jsx
│ ├── api/index.js
│ ├── utils/linkify.js
│ ├── hooks/
│ │ ├── useTodos.js
│ │ ├── useGroups.js
│ │ └── useDetails.js
│ └── components/
│ ├── TabBar.jsx
│ ├── TodoForm.jsx
│ ├── TodoCard.jsx
│ ├── DetailList.jsx
│ ├── DetailItem.jsx
│ ├── GroupModal.jsx
│ ├── BackupModal.jsx
│ └── ConfirmModal.jsx
└── .gitignore
node_modules/
dist/
*.db
*.db-shm
*.db-wal
*.db-journal
.env
{
"name": "todo-server",
"version": "1.0.0",
"description": "Todo App Backend - Express + SQLite",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node index.js"
},
"dependencies": {
"better-sqlite3": "^11.7.0",
"cors": "^2.8.5",
"express": "^4.21.0"
}
}
const express = require('express');
const cors = require('cors');
const todosRouter = require('./routes/todos');
const detailsRouter = require('./routes/details');
const groupsRouter = require('./routes/groups');
const backupRouter = require('./routes/backup');
const app = express();
const PORT = 3001;
app.use(cors());
app.use(express.json());
app.use('/api/todos', todosRouter);
app.use('/api', detailsRouter);
app.use('/api/groups', groupsRouter);
app.use('/api/backup', backupRouter);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
const Database = require('better-sqlite3');
const path = require('path');
const db = new Database(path.join(__dirname, 'todo.db'));
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
createdAt TEXT DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO groups (id, name) VALUES (1, '미지정');
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
groupId INTEGER DEFAULT 1,
completed INTEGER DEFAULT 0,
favorited INTEGER DEFAULT 0,
pinned INTEGER DEFAULT 0,
deleted INTEGER DEFAULT 0,
deletedAt TEXT,
createdAt TEXT DEFAULT (datetime('now')),
FOREIGN KEY (groupId) REFERENCES groups(id)
);
CREATE TABLE IF NOT EXISTS details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
todoId INTEGER NOT NULL,
content TEXT NOT NULL,
createdAt TEXT DEFAULT (datetime('now')),
updatedAt TEXT DEFAULT (datetime('now')),
FOREIGN KEY (todoId) REFERENCES todos(id) ON DELETE CASCADE
);
`);
// Migration: add pinned column if missing
try {
db.prepare("SELECT pinned FROM todos LIMIT 1").get();
} catch {
db.exec("ALTER TABLE todos ADD COLUMN pinned INTEGER DEFAULT 0");
}
module.exports = db;
정렬: ORDER BY t.pinned DESC, t.createdAt DESC
PATCH 허용 필드: title, groupId, completed, favorited, pinned
전체 코드는 references/server-todos.js를 참조. 핵심 포인트:
const insertTodo = db.prepare('INSERT INTO todos (id, title, groupId, completed, favorited, pinned, deleted, deletedAt, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
for (const t of todos) {
insertTodo.run(t.id, t.title, t.groupId || 1, t.completed, t.favorited, t.pinned || 0, t.deleted, t.deletedAt, t.createdAt);
}
{
"name": "todo-client",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.0"
}
}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:3001',
},
},
});
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,jsx}'],
theme: { extend: {} },
plugins: [],
};
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
word-break: break-word;
}
}
body {
@apply bg-gray-50 text-gray-900;
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
!isTrash &&)!isTrash &&일 때만 표시useMemo로 groups 기반 Map 생성, 미지정(id=1) 포함일반 카드:
items-start 정렬, lineHeight: '20px'paddingTop: '3px' wrappertext-base leading-5text-sm leading-5, 클릭 시 인라인 편집, linkify(title, searchTerm)w-3.5 h-3.5, paddingTop: '3px'text-orange-400 + fill[&:has(.todo-title-row:hover)]:bg-blue-50 [&:has(.todo-title-row:hover)]:border-blue-200bg-gray-50 border-gray-100 (미완료: bg-white border-gray-200)overflow-hidden 카드에 적용휴지통 카드:
min-w-[40px]/min-w-[70px] + whitespace-nowrap + shrink-0{navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl'}+Enter로 저장 안내{index + 1}.border-t border-dashed border-gray-200 (index > 0)<a> 태그 변환<mark class="bg-yellow-200 rounded px-0.5"> 하이라이트 (HTML 태그 내부 건너뜀)모든 API 호출 함수: getTodos, createTodo, updateTodo, deleteTodo, restoreTodo, permanentDeleteTodo, emptyTrash, purgeTodos, getDetails, createDetail, updateDetail, deleteDetail, getGroups, createGroup, updateGroup, deleteGroup, exportBackup, importBackup
모든 UI 텍스트는 한국어로 작성한다.