Imported from seongmock/ProjectHelper (
AGENTS.md). Install upstream withnpx skills add seongmock/ProjectHelper. Copyright stays with the author.
AGENTS.md โ ProjectHelper ๊ฐ๋ฐ์/AI ์์ด์ ํธ ๊ฐ์ด๋
ํ๋ก์ ํธ ๊ฐ์
React 18 + Vite ํ๋ก ํธ์๋ + ์ํ Express API(server/, ์์ํ๋ JSON ํ์ผ์ด ๊ธฐ๋ณธ์ด๊ณ ์ด์์ PH_STORE=sqlite) ๊ตฌ์กฐ์
ํ๋ก์ ํธ ํ์๋ผ์ธยท๊ฐํธ ์ฐจํธ ๊ด๋ฆฌ ๋๊ตฌ. ์คํ ๋ฆฌ์ง๋ localStorage ์บ์ + ์๋ฒ ๋๊ธฐํ ํ์ด๋ธ๋ฆฌ๋
(์๋ฒ ์์ด๋ localStorage ํด๋ฐฑ์ผ๋ก ๋์). AI ์์ด์ ํธ์ฉ REST/MCP ์ฐ๋ ํฌํจ.
์์ธ:
docs/ARCHITECTURE.mdยท AI ์ฐ๋:docs/AI_INTEGRATION.mdยท ๋ณ๊ฒฝ ์ด๋ ฅ:docs/REFACTORING_REPORT.md
๋น๋ / ๊ฐ๋ฐ ๋ช ๋ น์ด
npm run dev # ๊ฐ๋ฐ ์๋ฒ (http://localhost:5173, Hot-Reload, /api โ :3000 ํ๋ก์)
npm run dev:api # Express API ์๋ฒ (:3000) โ ์๋ฒ ๋๊ธฐํยทAI ์ฐ๋ ๊ธฐ๋ฅ์ ํ์
npm run build # ํ๋ก๋์
๋น๋ โ dist/
npm run test:unit # Vitest ๋จ์ 610๊ฑด (๋๋ฉ์ธ ์์ํจ์ + XSS ํ๊ท)
npm run test:server # node:test ์๋ฒ 356๊ฑด (๊ฒ์ฆยท์๋น์คยท์ ์ฅ์ยท์ธ์ฆยท๊ฐ์ฌ)
npm run test:e2e # Playwright E2E 130๊ฑด (APIยทdev ์๋ฒ ์๋ ๊ธฐ๋)
npm run test:e2e:sqlite # ๊ฐ์ E2E ๋ฅผ ์ด์ ์ ์ฅ ์์ง(SQLite)์ผ๋ก
npm run verify # ์ ์ ๋ถ + ๋น๋ โ ๋ณ๊ฒฝ ํ ์ด๊ฒ์ ๋๋ฆฐ๋ค
npm run preview # ๋น๋ ๊ฒฐ๊ณผ๋ฌผ ๋ก์ปฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
์ฝ๋ ๋ณ๊ฒฝ ํ
npm run verifyํ์ (lint 0 error + ๋จ์ 610๊ฑด + ์๋ฒ 356๊ฑด + ๋น๋ + E2E 130๊ฑด). E2E๋ playwright.config.js๊ฐ API ์๋ฒ๊น์ง ์๋ ๊ธฐ๋ํ๋ฏ๋ก skip์ด ๋ฐ์ํ์ง ์๋๋ค. skip์ด 1๊ฑด์ด๋ผ๋ ์์ผ๋ฉด ๊ทธ ํ ์คํธ๋ ์๋ฌด๊ฒ๋ ๊ฒ์ฆํ์ง ์์ ๊ฒ์ด๋ค โ ๋ถํฉ๊ฒฉ์ผ๋ก ๋ณธ๋ค.
์งํ ์ค์ธ ๊ฐ์ ์์ ์ํ์ ๋ฐ์ดํฐ ๊ด๋ จ ์ ๋ ๊ท์น์
HANDOVER.md๋ฅผ ๋จผ์ ์ฝ์ด๋ผ.
์ํคํ ์ฒ
์ํ ํ๋ฆ
App.jsx (์ค์ ์ํ ๊ด๋ฆฌ)
โโโ useUndoRedo(tasks) โ ์์
ํธ๋ฆฌ ์ ์ฒด๋ฅผ ํ์คํ ๋ฆฌ์ ํจ๊ป ๊ด๋ฆฌ
โโโ useToast() โ ์ ์ญ ํ ์คํธ ์๋ฆผ
โโโ useState(viewMode) โ 'table' | 'timeline' | 'split'
โโโ useState(settings...) โ timeScale, zoomLevel, showToday, ๋ฑ 10๊ฐ
โ
โโโ TableView โ ๊ณ์ธต์ ํ ํธ์ง
โโโ TimelineView โ ๊ฐํธ ์ฐจํธ + DnD (@dnd-kit)
โโโ TimelineBarPopover โ ์ฐํด๋ฆญ ์ปจํ
์คํธ ๋ฉ๋ด (์ ์ญ ํ์ค๋ฒ)
โโโ SaveLoadModal โ ๋ก์ปฌ ์ค๋
์ท ์ ์ฅ/๋ถ๋ฌ์ค๊ธฐ
โโโ ImportExportModal โ JSON ํ์ผ ๊ฐ์ ธ์ค๊ธฐ/๋ด๋ณด๋ด๊ธฐ
โโโ ToastContainer โ ํ๋ฉด ์ฐํ๋จ ์๋ฆผ
๋ฐ์ดํฐ ๋ชจ๋ธ
์์ (Task)์ ์ฌ๊ท ํธ๋ฆฌ:
{
id: 'task-1726...abc', // generateId()๋ก ์์ฑ
name: '์์
๋ช
',
timeRanges: [ // โ ์ฃผ์ ๋ ์ง ๋ฐ์ดํฐ (startDate/endDate๋ ๋ ๊ฑฐ์)
{
id: 'task-...',
startDate: 'YYYY-MM-DD',
endDate: 'YYYY-MM-DD',
dependencies: [], // ์ ํ rangeId ๋๋ taskId ๋ฐฐ์ด
color: null,
label: ''
}
],
color: '#4A90E2',
description: '',
children: [], // ํ์ ์์
(์ฌ๊ท)
expanded: true,
labels: [],
parentId: null,
milestones: [
{ id, date, label, color, shape: 'diamond'|'circle'|'triangle'|'square' }
],
dependencies: [], // ํญ์ [] (range ๋ ๋ฒจ๋ก ๋ง์ด๊ทธ๋ ์ด์
๋จ)
divider: { enabled, thickness, style, color }
}
์คํ ๋ฆฌ์ง (ํ์ด๋ธ๋ฆฌ๋)
์ฝ๊ธฐ: ์๋ฒ(/api/*) ์ฐ์ โ ์คํจ ์ localStorage ํด๋ฐฑ. ์ฐ๊ธฐ: localStorage ์ฆ์ + ์๋ฒ ์ ์ก(1.5s ๋๋ฐ์ด์ค).
| localStorage ํค | ๋ด์ฉ |
|---|---|
project-timeline-data |
ํ์ฑ ์์ ํธ๋ฆฌ ์บ์ |
project-timeline-settings |
UI ์ค์ (timeScale, zoomLevel, darkMode ๋ฑ) |
project-timeline-snapshots |
์ด๋ฆ ์ง์ ์ ์ฅ ์ฌ๋กฏ ์บ์ |
์๋ฒ ํ์ผ(server/data/): data.json(bare ๋ฐฐ์ด) ยท meta.json(revision โ ๋ชจ๋ ๋ณ๊ฒฝ๋ง๋ค +1) ยท
settings.json ยท snapshots.json
๋์์ฑ: ๋ธ๋ผ์ฐ์ ๋ ์ ์ฅ ์ If-Match: <revision> ์ ์ก(409 ์ ์๋ฒ ์ฐ์ ์ฌ๋ก๋),
10์ด๋ง๋ค GET /api/revision ํด๋ง์ผ๋ก ์ธ๋ถ(AI) ๋ณ๊ฒฝ์ ์๋ ๋ฐ์. ์ธ๋ถ ์ฌ๋ก๋๋
setTasksSilent๋ฅผ ์ฌ์ฉํ๋ฏ๋ก undo ํ์คํ ๋ฆฌ๋ฅผ ์ค์ผ์ํค์ง ์๋๋ค.
ํต์ฌ ์ ํธ๋ฆฌํฐ
src/utils/taskTree.js โ ํธ๋ฆฌ ์กฐ์ ์์ ํจ์ (ํ์ ์ฌ์ฉ)
| ํจ์ | ์ญํ |
|---|---|
updateTaskInTree(items, id, updates) |
์ฌ๊ท ๋ถ๋ณ ์ ๋ฐ์ดํธ |
deleteFromTree(items, id) |
์์ ํฌํจ ์ญ์ |
addToParent(items, parentId, task) |
ํ์ ์์ ์ถ๊ฐ (๋ถ๋ชจ ์๋ ํ์ฅ) |
findTaskAndParent(items, id) |
{task, parent, index, list} ํ์ |
indentTask / outdentTask |
๋ค์ฌ์ฐ๊ธฐ/๋ด์ด์ฐ๊ธฐ |
isDescendant(parent, id) |
์ํ ์ด๋ ๋ฐฉ์ง ๊ฒ์ฌ |
regenerateIds(items) |
๊ฐ์ ธ์ค๊ธฐ ๋ณํฉ ์ ID ์ฌ์์ฑ |
recalcTaskBounds(ranges) / recalcTaskBoundsSafe |
timeRanges โ ์ ์ฒด ์์/์ข ๋ฃ์ผ |
findOwnerOfEntity(flatList, id) |
task/range/milestone ID์ ์์ ์์ ํ์ |
ํธ๋ฆฌ ์ฌ๊ท๋ฅผ ์ปดํฌ๋ํธ ์์์ ๋ค์ ๊ตฌํํ์ง ๋ง ๊ฒ. โ ๏ธ
server/lib/taskTree.js๋ ์ด ํ์ผ์ CommonJS ๋ฏธ๋ฌ โ ์๊ทธ๋์ฒ ๋ณ๊ฒฝ ์ ํจ๊ป ๊ฐฑ์ .
src/utils/dataModel.js
| ํจ์ | ์ญํ |
|---|---|
createNewTask(name, parentId) |
์ฌ๋ฐ๋ฅธ ๊ตฌ์กฐ์ ์ ๊ท ์์ ์์ฑ. ์ง์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ง ๋ง ๊ฒ |
generateId() |
task-{timestamp}-{random} ํ์์ ๊ณ ์ ID ์์ฑ |
flattenTasks(tasks, level) |
ํธ๋ฆฌ๋ฅผ expanded ์ํ๋ฅผ ์กด์คํ๋ฉฐ ์์ ์๋ ๋ฐฐ์ด๋ก ํํํ |
migrateTaskData(tasks) |
๊ตฌ๋ฒ์ (startDate/endDate) โ timeRanges ์๋ ๋ณํ. ๋ก๋ ์ ํญ์ ํธ์ถ๋จ |
formatDate(date) |
Date ๊ฐ์ฒด ๋๋ ๋ฌธ์์ด์ YYYY-MM-DD๋ก ์ ๊ทํ |
src/shared/hooks/useUndoRedo.js
const { state, setState, setStateSilent, undo, redo, canUndo, canRedo } = useUndoRedo(initialState);
setState(fn)โ ํ์คํ ๋ฆฌ์ ๊ธฐ๋กํ๋ ์ํ ๋ณ๊ฒฝ (์ฌ์ฉ์ ์ก์ )setStateSilent(fn)โ ํ์คํ ๋ฆฌ์ ๊ธฐ๋กํ์ง ์๋ ๋ณ๊ฒฝ (๋๋๊ทธ ์ค๊ฐ ์ํ)- ํ์คํ ๋ฆฌ ์ต๋ 20๊ฐ ์ ์ง
src/shared/hooks/useToast.js
const { toasts, toast, removeToast } = useToast();
toast.success('๋ฉ์์ง');
toast.error('๋ฉ์์ง');
toast.warn('๋ฉ์์ง');
toast.info('๋ฉ์์ง');
alert() ๋์ ๋ฐ๋์ toast๋ฅผ ์ฌ์ฉํ ๊ฒ.
์ฃผ์ ๊ฐ๋ฐ ํจํด
์์ ํธ๋ฆฌ ์ ๋ฐ์ดํธ (๋ถ๋ณ์ฑ)
// โ
์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ: updateTaskInTree ์ฌ๊ท ํฌํผ ์ฌ์ฉ
const updateTaskInTree = (items, taskId, updates) =>
items.map(task =>
task.id === taskId
? { ...task, ...updates }
: { ...task, children: updateTaskInTree(task.children, taskId, updates) }
);
setTasks(prev => updateTaskInTree(prev, taskId, { name: '์ ์ด๋ฆ' }));
// โ ๊ธ์ง: ์๋ณธ ๊ฐ์ฒด ์ง์ ๋ณ๊ฒฝ
item.children = something; // ๋ฎคํ
์ด์
โ undo/redo ํ์คํ ๋ฆฌ ์ค์ผ
์์ ์ญ์ (๋ถ๋ณ์ฑ)
const deleteFromTree = (items) =>
items
.filter(item => item.id !== taskId)
.map(item => ({ ...item, children: deleteFromTree(item.children) }));
setTasks(prev => deleteFromTree(prev));
๋ฅํด๋ก
// โ
์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ
const cloned = structuredClone(obj);
// โ ๊ธ์ง (Date ๊ฐ์ฒด, undefined ์์ค ์ํ)
const cloned = JSON.parse(JSON.stringify(obj));
ํํฐ๋ง๋ ๋ชฉ๋ก
App.jsx์ filteredTasks๋ useMemo๋ก ๊ณ์ฐ๋ ๋ณ์.
JSX์์ ํจ์ ํธ์ถ filteredTasks()๊ฐ ์๋ ๋ณ์ ์ฐธ์กฐ filteredTasks ๋ก ์ฌ์ฉ.
์ปดํฌ๋ํธ-CSS ์
๋ชจ๋ ์ปดํฌ๋ํธ๋ ๊ฐ์ ์ด๋ฆ์ CSS ํ์ผ๊ณผ ์์ ์ด๋ฃธ:
Foo.jsx + Foo.css. ๋คํฌ ๋ชจ๋๋ [data-theme="dark"] ์ ํ์๋ก ์ฒ๋ฆฌ.
๋์ ๊ฒ์ฆ ๋ฐฉ๋ฒ
์๋ ๊ฒ์ฆ (๊ถ์ฅ โ ์๋ ์ฒดํฌ๋ฆฌ์คํธ ๋๋ถ๋ถ์ ์ปค๋ฒ)
npm run verify # lint + ๋จ์ 610๊ฑด + ์๋ฒ 356๊ฑด + ๋น๋ + E2E 130๊ฑด
npm run test:e2e:sqlite # ์ด์ ์ ์ฅ ์์ง(SQLite)์ผ๋ก ๊ฐ์ E2E ๋ฅผ ํ ๋ฒ ๋
๋น๋ ๊ฒ์ฆ
npm run build
# ์ค๋ฅ ์์ด dist/ ์์ฑ ํ์ธ
์๋ ๊ฒ์ฆ ์ฒดํฌ๋ฆฌ์คํธ
์์ CRUD:
- "์ ์์ " ์ถ๊ฐ โ ๋ชฉ๋ก์ ํ์
- ์์ ๋ช ๋๋ธํด๋ฆญ โ ์ธ๋ผ์ธ ํธ์ง โ Enter ์ ์ฅ
- ์์ ์ญ์ โ ์์ ํฌํจ ์ญ์ , ์ ํ ํด์
- Ctrl+Z / Ctrl+Y โ ์คํ ์ทจ์/๋ค์ ์คํ
๋ค๋จ๊ณ ์ค์ฒฉ ์ ๋ฐ์ดํธ (3๋จ๊ณ ์ด๊ณผ):
- ์์ > ํ์ > ํ์ > ํ์ (4๋จ๊ณ) ์์ ๋ช ์์ โ ์ ์ ๋ฐ์
์ ์ฅ/๋ถ๋ฌ์ค๊ธฐ:
-
Ctrl+Sโ JSON ํ์ผ ๋ค์ด๋ก๋ - ๊ฐ์ ธ์ค๊ธฐ โ ๋ฎ์ด์ฐ๊ธฐ/๋ณํฉ ๊ฐ๊ฐ ๋์
- ๐พ ์ ์ฅ ๋ชจ๋ฌ โ ์ ์ฅ โ ๋ถ๋ฌ์ค๊ธฐ โ ๋ฐ์ดํฐ ๋ณต์
Toast:
- ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต โ ํ๋จ ์ฐ์ธก ํ ์คํธ ํ์ (alert ์ฐจ๋จ ์์)
- ์๋ชป๋ JSON ๊ฐ์ ธ์ค๊ธฐ โ ์๋ฌ ํ ์คํธ
๊ฒ์:
- ์์ ๋ช ๊ฒ์ โ ๋งค์นญ ํญ๋ชฉ๋ง ํ์
- description ๋ด์ฉ ๊ฒ์ โ ๋งค์นญ ํญ๋ชฉ ํ์
ํ์๋ผ์ธ ๋๋๊ทธ:
- ๋ฐ ๋๋๊ทธ โ ๋ ์ง ๋ณ๊ฒฝ ํ undo ๊ฐ๋ฅ
- Ctrl ๋๋๊ทธ โ ๋ณต์ฌ ๋ชจ๋
- Escape โ ๋๋๊ทธ ์ทจ์
์ด๋ฏธ์ง ์บก์ฒ:
- HTTPS ํ๊ฒฝ: ํด๋ฆฝ๋ณด๋ ๋ณต์ฌ ์ฑ๊ณต ํ ์คํธ
- HTTP ํ๊ฒฝ: ๋ค์ด๋ก๋ ํ์ผ ์์ฑ + ์ ๋ณด ํ ์คํธ
ํ์ผ ๊ตฌ์กฐ ์์ฝ
๊ธฐ๋ฅ๋ณ๋ก ๋ฌถ๋๋ค(feature-first). ์ปดํฌ๋ํธยทํ
ยท๊ทธ ๊ธฐ๋ฅ์์๋ง ์ฐ๋ ์ ํธ์ ํ ํด๋์ ๋๋ค.
์ฌ๋ฌ ๊ธฐ๋ฅ์ด ๊ณต์ ํ๋ ๊ฒ๋ง shared/ ๋ก ์ฌ๋ฆฐ๋ค.
src/
โโโ App.jsx # ์ค์ ์ํ, ์ ์ฒด ํธ๋ค๋ฌ
โโโ features/
โ โโโ shell/ # Header, Toolbar, DisplayOptionsMenu (์ฑ ๊ป๋ฐ๊ธฐ)
โ โโโ table/ # TableView, TaskRow (ํ ๋ทฐ + @dnd-kit ์ ๋ ฌ)
โ โโโ timeline/ # TimelineView/Bar/Header/BarPopover, DependencyLayer,
โ โ # MilestoneQuickAdd,
โ โ # useBarDrag/useDependencyLink/useTimelineScale/
โ โ # useTimelineCapture/useSidebarResize,
โ โ # timelineGeometry.js, timelineMutations.js
โ โโโ tasks/ # useTaskActions (์์
CRUD ํธ๋ค๋ฌ)
โ โโโ projects/ # ProjectRail, ProjectManagerModal, useProjectSync
โ โโโ io/ # ImportExportModal, SaveLoadModal, PromptGuideModal,
โ # useImportExport, htmlExporter.js
โโโ shared/
โ โโโ ui/ # Modal, Toast, Tooltip, ColorPicker, ErrorBoundary
โ โโโ hooks/ # useUndoRedo, useToast
โโโ stores/ # settingsStore, uiStore (Zustand)
โโโ themes/ # ์ฐจํธ ์์ ํ
๋ง
โโโ utils/ # ๋๋ฉ์ธ ์ฝ์ด โ ํน์ ๊ธฐ๋ฅ ์์ ๊ฐ ์๋๋ค
โโโ dataModel.js # Task ๊ตฌ์กฐ ์ ์, ๋ง์ด๊ทธ๋ ์ด์
, ์ํ ๋ฐ์ดํฐ
โโโ taskTree.js # ๋ถ๋ณ ํธ๋ฆฌ ์กฐ์ ์์ํจ์
โโโ dateUtils.js # ๋ ์ง ๊ณ์ฐ/์ค๋
/ํฌ๋งท
โโโ storage.js # ์๋ฒ ๋๊ธฐํ + localStorage ์บ์, ์ค๋
์ท ๊ด๋ฆฌ
์ด๋์ ๋์ง ํ๋จํ๋ ๊ท์น: ํ ๊ธฐ๋ฅ๋ง ์ฐ๋ฉด ๊ทธ features/<name>/ ์์, ๋ ๊ธฐ๋ฅ ์ด์์ด
์ฐ๋ฉด shared/, ๋ฐ์ดํฐ ๋ชจ๋ธยทํธ๋ฆฌยท๋ ์งยท์ ์ฅ์ฒ๋ผ ์ ๊ณ์ธต์ด ์ฐ๋ ์ฝ์ด๋ utils/.
๋ฐฐํฌ
# HTTP ๊ธฐ๋ณธ ๋ฐฐํฌ
./start_server.sh
# HTTPS (๋ด๋ถ๋ง, Caddy ์ฌ์ฉ)
# Caddyfile์ IP๋ฅผ ์๋ฒ IP๋ก ์์ ํ:
./start_https.sh
# ๊ฐ๋ฐ ๋ชจ๋ (Hot-Reload)
./start_dev.sh
HTTPS ํ๊ฒฝ์์๋ง ํด๋ฆฝ๋ณด๋ Write API (navigator.clipboard.write)๊ฐ ๋์.
HTTP ํ๊ฒฝ์์๋ ์ด๋ฏธ์ง ์บก์ฒ ์ ์๋์ผ๋ก ํ์ผ ๋ค์ด๋ก๋๋ก ๋์ฒด.