Jacalμ μΌμ , ν μΌ, μ΅κ΄ μΆμ , ν νμ , μμ°μ± λΆμμ νλλ‘ ν΅ν©ν μ¬μΈμ μμ°μ± νλ«νΌμ λλ€.
- π£οΈ μμ°μ΄ μ λ ₯: ν μ€λ‘ μΌμ /ν μΌ μλ λΆλ₯ λ° μμ±
- π€ AI νμ±: OpenAI/Ollamaλ₯Ό νμ©ν μ€λ§νΈ μμ°μ΄ μ²λ¦¬
- π μΊλ¦°λ λ·°: μκ°/μ£Όκ° μΊλ¦°λ 보기 μ§μ
- β ν μΌ κ΄λ¦¬: μ°μ μμ, μμ μμ μκ°, μν κ΄λ¦¬
- π― μ΅κ΄ μΆμ : μΌμΌ/μ£Όκ° μ΅κ΄ κ΄λ¦¬ λ° μλ£ κΈ°λ‘
- π₯ ν νμ : νλ³ κ³΅μ μΌμ λ° λκΈ κΈ°λ₯
- β‘ μ§μ€ νμ΄λ¨Έ: ν¬λͺ¨λλ‘ νμ΄λ¨Έλ‘ μ§μ€λ ₯ ν₯μ
- π μμ°μ± λΆμ: μΌμΌ μμ°μ± μΆμ λ° μκ°ν
- π 보μ μΈμ¦: JWT κΈ°λ° μμ ν μΈμ¦ μμ€ν
- π λ€κ΅μ΄ μ§μ: νκ΅μ΄/μμ΄ μ§μ (i18next)
- β¨οΈ ν€λ³΄λ λ¨μΆν€: λ§μ°μ€ μμ΄ λΉ λ₯Έ μμ (Ctrl+K λ±)
- π§ μ΄λ©μΌ ν΅ν©: POP3λ₯Ό ν΅ν μ΄λ©μΌ μλ νμ±
- π μΉν μμ€ν : μΈλΆ μμ€ν μ°λ
- π οΈ κ΄λ¦¬μ ν¨λ: μ 체 μμ€ν κ΄λ¦¬ λ° λͺ¨λν°λ§
- Runtime: Node.js + Express + TypeScript
- Database: PostgreSQL + Prisma ORM
- AI/NLP: OpenAI API, Ollama (μ νμ )
- Authentication: JWT + bcrypt
- Email: POP3 (node-pop3), Nodemailer
- Schedule: node-cron
- Validation: Zod
- Calendar: Google Calendar API
- Framework: React 19 + TypeScript + Vite
- State: TanStack Query (React Query)
- HTTP Client: Axios
- i18n: i18next, react-i18next
- Charts: Recharts
- Styling: Modern CSS (CSS Variables)
- Container: Docker Compose
- Database: PostgreSQL 15 Alpine
- Node.js v18 μ΄μ
- Docker & Docker Compose (κΆμ₯)
- OpenAI API Key (μ νμ - Ollama μ¬μ© κ°λ₯)
- λ ν¬μ§ν 리 ν΄λ‘
git clone <repo-url>
cd jacal- PostgreSQL μ€ν
docker-compose up -d- μμ‘΄μ± μ€μΉ
npm install- νκ²½ λ³μ μ€μ
apps/api/.env νμΌ μμ±:
PORT=3000
DATABASE_URL="postgresql://jacal:jacal123@localhost:5432/jacal?schema=public"
JWT_SECRET=your-super-secret-key-change-in-production
OPENAI_API_KEY=your-openai-api-key-here # μ νμ - λ°μ΄ν°λ² μ΄μ€ λ§μ΄κ·Έλ μ΄μ
cd apps/api
npx prisma migrate dev
npx prisma generate- κ΄λ¦¬μ κ³μ μμ± (μ νμ )
npm run create-admin- λ°±μλ μλ² μ€ν
npm run dev- νλ‘ νΈμλ μ€ν (μ ν°λ―Έλ)
cd apps/web
npm run dev- λΈλΌμ°μ μμ μ μ
- Frontend: http://localhost:5173
- Backend API: http://localhost:3000
μ κ³Όμ μμ 2λ² λμ λ‘컬 PostgreSQLμ μ¬μ©νκ³ , DATABASE_URLμ μ μ ν μμ νμΈμ.
ν μ€μ μμ°μ΄ μ λ ₯μΌλ‘ μ΄λ²€νΈλ ν μΌμ μλ μμ±ν©λλ€.
μμ:
- "λ΄μΌ μ€ν 3μ ν λ―Έν 2μκ°"
- "λ€μ μ£Ό κΈμμΌκΉμ§ λ³΄κ³ μ μμ±"
- "λ§€μΌ μμΉ¨ 9μ μ΄λνκΈ°"
μλν¬μΈνΈ: POST /api/nlu/parse
- μκ° λ³΄κΈ°: μ μ 체μ μΌμ νλμ νμ
- μ£Όκ° λ³΄κΈ°: μκ°λλ³ μμΈ μΌμ νμΈ
- κ΄λ¦¬μ λͺ¨λ: μ 체 μ¬μ©μμ μΌμ μ‘°ν κ°λ₯
- μΌμΌ/μ£Όκ° μ΅κ΄ μμ± λ° κ΄λ¦¬
- μλ£ κΈ°λ‘ λ° λ¬μ±λ₯ νμΈ
- μμ/μμ΄μ½μΌλ‘ μκ°μ ꡬλΆ
API:
GET /api/habits- μ΅κ΄ λͺ©λ‘POST /api/habits- μ΅κ΄ μμ±POST /api/habits/:id/log- μλ£ κΈ°λ‘GET /api/habits/:id/stats- ν΅κ³ μ‘°ν
- ν μμ± λ° λ©€λ² κ΄λ¦¬ (Owner/Admin/Member μν )
- 곡μ μ΄λ²€νΈ μμ± λ° λκΈ
- νμ κ° μΌμ μ‘°μ¨
API:
GET /api/teams- ν λͺ©λ‘POST /api/teams- ν μμ±POST /api/teams/:id/events- 곡μ μ΄λ²€νΈ μμ±POST /api/teams/events/:id/comments- λκΈ μμ±
- μΌμΌ μ§μ€ μκ° μΆμ
- μλ£ν ν μΌ ν΅κ³
- λ―Έν μκ° λΆμ
- μμ°μ± μ μ κ³μ°
API:
GET /api/analytics/dashboard- λμ보λ λ°μ΄ν°GET /api/analytics/productivity- μμ°μ± μΆμ΄
κ΄λ¦¬μ μ¬μ©μλ λ€μ κΈ°λ₯μ μ κ·Ό κ°λ₯:
- μμ€ν ν΅κ³: μ¬μ©μ/μ΄λ²€νΈ/ν μΌ μ§κ³
- μ¬μ©μ κ΄λ¦¬: μ 체 μ¬μ©μ μ‘°ν/μμ /μμ , POP3 μ€μ
- μ½ν μΈ κ΄λ¦¬: μ 체 ν μΌ/μ΄λ²€νΈ/μ΅κ΄/ν κ΄λ¦¬
- λ°μ΄ν°λ² μ΄μ€: ν΅κ³ λ° λ°±μ κ΄λ¦¬
- μ€μ : μ¬μ΄νΈ μ΄λ¦, URL, μΈμ΄, κ°μ νμ© λ±
- μΉν κ΄λ¦¬: μΉν CRUD
- ν΅ν© κ΄λ¦¬: μΈλΆ μλΉμ€ μ°λ
- μ΄λ©μΌ μ€μ : SMTP μ€μ
κ° μ¬μ©μλ κ°μΈ μ€μ κ°λ₯:
- Ollama μ€μ : OpenAI λμ λ‘컬 LLM μ¬μ©
- POP3 μ€μ : μ΄λ©μΌ μλ νμ± (μ λͺ©/λ³Έλ¬Έμμ ν μΌ μμ±)
- μΉν μ€μ : κ°μΈ μΉν URL λ° μ»¬λΌ λ§€ν
Ctrl+K: λͺ λ Ή νλ νΈ (λΉ λ₯Έ μμ )N: μ μ΄λ²€νΈT: μ ν μΌD: λμ보λC: μΊλ¦°λH: μ΅κ΄ μΆμ M: ν λ·°S: μ€μ ?: λ¨μΆν€ λμλ§
POST /api/auth/register- νμκ°μPOST /api/auth/login- λ‘κ·ΈμΈGET /api/auth/me- νμ¬ μ¬μ©μ μ 보
GET /api/tasks- ν μΌ λͺ©λ‘POST /api/tasks- ν μΌ μμ±PUT /api/tasks/:id- ν μΌ μμ DELETE /api/tasks/:id- ν μΌ μμ
GET /api/events- μ΄λ²€νΈ λͺ©λ‘GET /api/events/all- μ 체 μ΄λ²€νΈ (κ΄λ¦¬μ)POST /api/events- μ΄λ²€νΈ μμ±PUT /api/events/:id- μ΄λ²€νΈ μμ DELETE /api/events/:id- μ΄λ²€νΈ μμ
GET /api/habits- μ΅κ΄ λͺ©λ‘POST /api/habits- μ΅κ΄ μμ±PUT /api/habits/:id- μ΅κ΄ μμ DELETE /api/habits/:id- μ΅κ΄ μμ POST /api/habits/:id/log- μλ£ κΈ°λ‘GET /api/habits/:id/stats- ν΅κ³
GET /api/teams- λ΄ ν λͺ©λ‘POST /api/teams- ν μμ±PUT /api/teams/:id- ν μμ DELETE /api/teams/:id- ν μμ POST /api/teams/:id/members- λ©€λ² μΆκ°DELETE /api/teams/:id/members/:userId- λ©€λ² μ κ±°GET /api/teams/:id/events- 곡μ μ΄λ²€νΈ λͺ©λ‘POST /api/teams/:id/events- 곡μ μ΄λ²€νΈ μμ±POST /api/teams/events/:id/comments- λκΈ μμ±
GET /api/analytics/dashboard- λμ보λGET /api/analytics/productivity- μμ°μ± μΆμ΄
GET /api/settings- λ΄ μ€μ PUT /api/settings- μ€μ μ λ°μ΄νΈ
GET /api/calendar- μΊλ¦°λ μ΄λ²€νΈ
POST /api/focus/session- μ§μ€ μΈμ κΈ°λ‘
POST /api/nlu/parse- μμ°μ΄ νμ± λ° μλ μμ±
GET /api/admin/stats- μμ€ν ν΅κ³GET /api/admin/users- μ¬μ©μ λͺ©λ‘PUT /api/admin/users/:id- μ¬μ©μ μμ DELETE /api/admin/users/:id- μ¬μ©μ μμ PUT /api/admin/users/:id/settings- μ¬μ©μ μ€μ μμ GET /api/admin/content/tasks- μ 체 ν μΌGET /api/admin/content/habits- μ 체 μ΅κ΄GET /api/admin/content/teams- μ 체 νDELETE /api/admin/content/:type/:id- μ½ν μΈ μμ GET /api/admin/database/stats- λ°μ΄ν°λ² μ΄μ€ ν΅κ³POST /api/admin/database/backup- λ°±μ μμ±GET /api/admin/database/backups- λ°±μ λͺ©λ‘GET /api/admin/settings- μ± μ€μ PUT /api/admin/settings- μ± μ€μ μ λ°μ΄νΈGET /api/admin/webhooks- μΉν λͺ©λ‘POST /api/admin/webhooks- μΉν μμ±PUT /api/admin/webhooks/:id- μΉν μμ DELETE /api/admin/webhooks/:id- μΉν μμ GET /api/admin/integrations- ν΅ν© λͺ©λ‘PUT /api/admin/integrations/:name- ν΅ν© μ λ°μ΄νΈGET /api/admin/email- μ΄λ©μΌ μ€μ PUT /api/admin/email- μ΄λ©μΌ μ€μ μ λ°μ΄νΈ
jacal/
βββ apps/
β βββ api/ # λ°±μλ
β β βββ src/
β β β βββ routes/ # API λΌμ°νΈ
β β β β βββ admin/ # κ΄λ¦¬μ λΌμ°νΈ
β β β β βββ auth.ts # μΈμ¦
β β β β βββ tasks.ts # ν μΌ
β β β β βββ events.ts # μ΄λ²€νΈ
β β β β βββ habits.ts # μ΅κ΄
β β β β βββ teams.ts # ν
β β β β βββ analytics.ts # λΆμ
β β β β βββ settings.ts # μ€μ
β β β β βββ calendar.ts # μΊλ¦°λ
β β β β βββ focus.ts # μ§μ€ νμ΄λ¨Έ
β β β β βββ nlu.ts # μμ°μ΄ μ²λ¦¬
β β β βββ services/ # λΉμ¦λμ€ λ‘μ§
β β β β βββ nlu.service.ts # NLU μλΉμ€
β β β β βββ webhook.service.ts # μΉν
μλΉμ€
β β β β βββ email.service.ts # μ΄λ©μΌ μλΉμ€
β β β βββ middleware/ # λ―Έλ€μ¨μ΄
β β β β βββ auth.ts # μΈμ¦ λ―Έλ€μ¨μ΄
β β β β βββ admin.ts # κ΄λ¦¬μ λ―Έλ€μ¨μ΄
β β β βββ lib/ # μ νΈλ¦¬ν°
β β β βββ index.ts # μνΈλ¦¬ ν¬μΈνΈ
β β βββ prisma/
β β β βββ schema.prisma # λ°μ΄ν°λ² μ΄μ€ μ€ν€λ§
β β βββ scripts/
β β β βββ create-admin.ts # κ΄λ¦¬μ μμ± μ€ν¬λ¦½νΈ
β β βββ package.json
β βββ web/ # νλ‘ νΈμλ
β βββ src/
β β βββ components/ # React μ»΄ν¬λνΈ
β β β βββ admin/ # κ΄λ¦¬μ μ»΄ν¬λνΈ
β β β βββ settings/ # μ€μ μ»΄ν¬λνΈ
β β β βββ Dashboard.tsx # λμ보λ
β β β βββ Calendar.tsx # μΊλ¦°λ
β β β βββ HabitTracker.tsx # μ΅κ΄ μΆμ
β β β βββ TeamView.tsx # ν λ·°
β β β βββ ...
β β βββ lib/ # API ν΄λΌμ΄μΈνΈ
β β β βββ api.ts # κΈ°λ³Έ API
β β β βββ habitApi.ts # μ΅κ΄ API
β β β βββ teamApi.ts # ν API
β β β βββ analyticsApi.ts # λΆμ API
β β β βββ adminApi.ts # κ΄λ¦¬μ API
β β βββ i18n/ # λ€κ΅μ΄
β β β βββ i18n.ts # i18next μ€μ
β β β βββ ko.json # νκ΅μ΄
β β β βββ en.json # μμ΄
β β βββ App.tsx # λ©μΈ μ»΄ν¬λνΈ
β β βββ index.css # κΈλ‘λ² μ€νμΌ
β β βββ main.tsx # μνΈλ¦¬ ν¬μΈνΈ
β βββ package.json
βββ docker-compose.yml # Docker μ€μ
βββ package.json # λ£¨νΈ ν¨ν€μ§
βββ README.md
μ£Όμ λͺ¨λΈ:
- User: μ¬μ©μ (μΈμ¦, κΆν)
- Task: ν μΌ
- Event: μ΄λ²€νΈ
- Habit: μ΅κ΄
- HabitLog: μ΅κ΄ μλ£ κΈ°λ‘
- Team: ν
- TeamMember: ν λ©€λ²
- SharedEvent: 곡μ μ΄λ²€νΈ
- Comment: λκΈ
- Tag: νκ·Έ
- Analytics: μμ°μ± λΆμ λ°μ΄ν°
- UserSettings: μ¬μ©μ μ€μ (Ollama, POP3)
- WebhookConfig: μΉν μ€μ
- ConnectedAccount: μΈλΆ κ³μ μ°λ (Google Calendar λ±)
- RecurringRule: λ°λ³΅ κ·μΉ
- Reminder: μλ¦Ό
- ProcessedEmail: μ²λ¦¬λ μ΄λ©μΌ κΈ°λ‘
- AppSettings: μ± μ μ μ€μ
- Webhook: κ΄λ¦¬μ μΉν
- Integration: ν΅ν© μ€μ
- EmailSettings: μ΄λ©μΌ μ€μ
- BackupRecord: λ°±μ κΈ°λ‘
# λ°±μλ κ°λ° μλ² (hot reload)
cd apps/api
npm run dev
# νλ‘ νΈμλ κ°λ° μλ² (hot reload)
cd apps/web
npm run dev# λ°±μλ λΉλ
cd apps/api
npm run build
npm start
# νλ‘ νΈμλ λΉλ
cd apps/web
npm run build
npm run preview# λ§μ΄κ·Έλ μ΄μ
μμ±
npx prisma migrate dev --name migration_name
# Prisma ν΄λΌμ΄μΈνΈ μ¬μμ±
npx prisma generate
# Prisma Studio (GUI)
npx prisma studio
# κ΄λ¦¬μ κ³μ μμ±
npm run create-admin# μλ² μ€μ
PORT=3000
NODE_ENV=development
# λ°μ΄ν°λ² μ΄μ€
DATABASE_URL="postgresql://jacal:jacal123@localhost:5432/jacal?schema=public"
# JWT μΈμ¦
JWT_SECRET=your-super-secret-key-change-in-production
# OpenAI (μ νμ )
OPENAI_API_KEY=sk-...
# Google Calendar (μ νμ )
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/google/callback
# SMTP (μ νμ - μ΄λ©μΌ λ°μ‘μ©)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password- νλ‘μ νΈ μ΄κΈ° μ€μ
- PostgreSQL + Prisma μ€μ
- JWT μΈμ¦ λ° κΆν κ΄λ¦¬
- ν μΌ CRUD
- μ΄λ²€νΈ CRUD
- μμ°μ΄ μ²λ¦¬ (OpenAI/Ollama)
- 리μ‘νΈ UI ꡬν
- μ΅κ΄ μΆμ μμ€ν
- ν νμ κΈ°λ₯
- μΊλ¦°λ λ·° (μκ°/μ£Όκ°)
- μμ°μ± λΆμ λμ보λ
- μ§μ€ νμ΄λ¨Έ
- κ΄λ¦¬μ ν¨λ (μ 체 κ΄λ¦¬)
- λ€κ΅μ΄ μ§μ (i18n)
- ν€λ³΄λ λ¨μΆν€
- POP3 μ΄λ©μΌ ν΅ν©
- μΉν μμ€ν
- μ¬μ©μ μ€μ
- Docker Compose μ€μ
- νκ·Έ μμ€ν
- λ°λ³΅ μΌμ μ§μ
- λκΈ κΈ°λ₯
- Google Calendar μλ°©ν₯ λκΈ°ν
- Outlook Calendar μ°λ
- CalDAV μ§μ
- νΈμ μλ¦Ό (μΉ/λͺ¨λ°μΌ)
- μ΄λ©μΌ μλ¦Ό
- SMS μλ¦Ό
- μ€λ§νΈ μ€μΌμ€λ§ μκ³ λ¦¬μ¦
- AI κΈ°λ° μκ° μΆμ²
- λͺ¨λ°μΌ μ± (React Native)
- νμΌ μ²¨λΆ κΈ°λ₯
- ν μΊλ¦°λ ν΅ν© λ·°
- λΉλμ€ νμ ν΅ν© (Zoom, Meet)
- κ³ κΈ λΆμ λ° λ¦¬ν¬νΈ
- 곡μ λ§ν¬ μμ±
- μ΄λ©μΌ μ΄λ μμ€ν
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
MIT License
νλ‘μ νΈ κ΄λ ¨ λ¬Έμμ¬νμ΄ μμΌμλ©΄ μ΄μλ₯Ό μμ±ν΄μ£ΌμΈμ.