규칙 엔진이 학습 일정의 뼈대를 만들고, Gemini가 그 뼈대 안의 내용만 채우는 D-Day 학습 플래너
▶ 데모: https://mukjin.github.io/FocusFlow_AI/
GitHub Pages는 정적 호스팅이라 서버 함수를 돌릴 수 없어, 이 데모는 규칙 엔진 전용입니다(모든 날짜에 같은 문구). AI 구체화까지 보려면 로컬 실행 또는 아래 "서버 함수로 배포하기"를 따라주세요.
AI에게 학습 계획을 통째로 맡기면 물어볼 때마다 결과 구조가 달라지고 요청한 일수와 실제 일정 개수가 맞지 않습니다. FocusFlow_AI는 날짜·시간대·소요시간·학습 단계를 결정론적 규칙으로 먼저 확정한 뒤, LLM에게는 각 칸의 할 일 문구와 참고 링크만 채우게 해서 일정 구조가 깨지지 않도록 만든 브라우저 단독 실행 플래너입니다.
필요한 것: Node.js 18 이상
git clone https://github.com/Mukjin/FocusFlow_AI.git
cd FocusFlow_AI
npm install
cp .env.example .env.local # 키를 여기에 채웁니다
npm run devhttp://localhost:3000 에서 열립니다.
.env.local 에 한 줄만 넣으면 됩니다. 앱 화면에서 키를 입력하는 곳은 없습니다.
VITE_GEMINI_API_KEY=여기에_발급받은_키Google AI Studio에서 카드 등록 없이 무료로 발급받습니다.
키가 없어도 앱은 정상 실행되며, 규칙 엔진이 만든 기본 일정이 나옵니다. 다만 모든 날짜에 같은 문구가 들어가고, Gemini가 날짜별 구체적인 주제로 바꿔주는 단계만 생략됩니다.
이 앱은 두 가지 모드로 동작하며, 빌드 시점에 어느 쪽인지 결정됩니다.
키 위치 언제 직접 모드 VITE_GEMINI_API_KEY(브라우저)로컬 개발 프록시 모드 GEMINI_API_KEY(서버 전용)배포
VITE_접두사가 붙은 값은 번들에 그대로 들어가 공개됩니다. 그래서 배포본은 서버 함수api/gemini.ts가 키를 쥐고 대신 호출합니다. 브라우저에는 키가 전혀 내려가지 않습니다.두 변수를 동시에 설정하면 빌드가 실패합니다. 실수로 프록시 배포에 클라이언트 키를 섞어 넣는 것을 막기 위한 장치입니다.
GitHub Pages는 정적 호스팅이라 서버 함수를 돌릴 수 없습니다. Vercel처럼 서버리스를 지원하는 곳에 올려야 배포본에서도 AI가 동작합니다.
npm i -g vercel # 이미 설치돼 있으면 생략
vercel login # 브라우저가 열립니다
vercel link # 프로젝트 연결 (처음 한 번)
# 서버 전용 키 — VITE_ 접두사를 붙이지 마세요
vercel env add GEMINI_API_KEY production
# 클라이언트가 호출할 프록시 경로
vercel env add VITE_AI_ENDPOINT production # 값: /api/gemini
vercel --prodapi/gemini.ts 가 하는 일:
- 키를 서버에만 두고 Gemini를 대신 호출
- 프롬프트 조립도 서버에서 — 남이 이 엔드포인트로 임의의 프롬프트를 밀어넣어 키를 태워 쓰는 것을 막습니다. 클라이언트는 '과목·단계별 개수'만 보냅니다
- 입력 제한(본문 16KB, 목표 12개, 항목 400개)과 IP 단위 빈도 제한(분당 6회)
- 기본은 같은 도메인에서만 호출 가능. 함수를 따로 배포할 때만
ALLOWED_ORIGIN설정
엣지 함수는 인스턴스마다 메모리가 따로라 빈도 제한이 완전하지 않습니다. 트래픽이 많아지면 KV/Redis 기반 제한이나 인증을 붙여야 합니다.
설정하지 않으면 저장 기능만 비활성화되고 나머지는 정상 동작합니다.
VITE_SUPABASE_URL=https://xxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-keySupabase SQL Editor에서 테이블을 먼저 만들어야 합니다.
create table if not exists planner_data (
user_id text primary key,
state jsonb not null default '{}'::jsonb,
updated_at timestamptz not null default now()
);
alter table planner_data enable row level security;
-- 로그인이 없는 익명 기기 ID 방식이라 anon 키로 읽고 쓸 수 있어야 합니다.
create policy "anon read write" on planner_data
for all using (true) with check (true);
⚠️ 위 정책은 모든 행을 anon 키로 접근 가능하게 열어둡니다. 개인 프로젝트 범위에서만 사용하세요.
npm run build # 프로덕션 빌드
npm run lint # 타입 검사 (tsc --noEmit)
npm run preview # 빌드 결과 미리보기사용자 입력 (D-Day · 목표 · 중요도 · 하루 시간 · 쉬는 요일)
│
▼
① 규칙 엔진 src/lib/ruleEngine.ts
· D-Day를 날짜별로 분할하고 쉬는 요일 제외
· 진행률 40% / 75% 지점을 경계로 기초 → 심화 → 마무리 단계 배정
· 목표별 중요도(1·2·3) 가중치로 하루 시간을 비례 배분
· 선호 시간대를 시작 시각으로 삼아 시작·종료 시각 계산
│
▼ ← 이 시점에 일정 개수·날짜·시간이 모두 확정된다
│
② Gemini src/lib/geminiClient.ts (①이 화면에 뜬 뒤 백그라운드에서 진행)
· 과목·단계별로 필요한 task 개수를 집계해 프롬프트에 명시
· 날짜마다 다른 구체적인 학습 주제를 요청 (반복 문구 금지)
· 응답을 shift()로 하나씩 소비해 뼈대에 주입
· 실패하거나 개수가 모자라도 ①의 일정이 그대로 남는다
│
▼
Zustand 스토어 → 캘린더 / 칸반 / 통계 / 목록 뷰
│
└─ (Supabase 설정 시) 1.5초 디바운스 후 전체 스냅샷 upsert
핵심은 ②가 실패해도 ①의 결과가 살아남는다는 점입니다. SetupForm.tsx의 handleGenerate가 Gemini 호출을 별도 try/catch로 감싸고, 실패 시 규칙 엔진 일정을 그대로 저장한 뒤 화면 상단 배너로 알립니다.
| 기능 | 위치 |
|---|---|
| 규칙 기반 일정 생성 (단계·시간 배분·쉬는 요일·토요일 1.5배) | src/lib/ruleEngine.ts |
| 목표 성격별 기본 문구 (시험형 / 어학형 / 독서형) | src/lib/ruleEngine.ts |
| 주간 복습 자동 배치 — 7일마다 그 주 학습을 되짚는 일정 삽입 | src/lib/ruleEngine.ts |
| 연속 학습일·최근 14일 활동 — 완료 시각 기준 실제 기록 | src/lib/streak.ts |
| Gemini 할 일 구체화 (규칙 엔진의 반복 문구를 날짜별 구체 주제로 교체) | src/lib/geminiClient.ts |
| 2단계 파이프라인 진행 표시 · AI 실패 시 폴백 배너 | src/components/SetupForm.tsx, src/App.tsx |
| 규칙 엔진 결과를 먼저 보여주고 AI 구체화는 백그라운드 진행 | src/store/plannerStore.ts (refineWithAI) |
| 월간/주간 캘린더, 드래그로 날짜 이동, 가로 드래그로 소요시간 리사이즈 | src/components/CalendarView.tsx |
| 일자 상세 패널 (일정 추가·수정·삭제) | src/components/DayDetailPanel.tsx |
| 칸반 보드 (예정 / 오늘 / 완료) 드래그 이동 | src/components/KanbanView.tsx |
| 통계 (달성률, 과목별 진행률, 계획 대비 완료 시간) | src/components/DashboardView.tsx |
| 과목별 목록 뷰 | src/components/ListView.tsx |
| 뽀모도로 타이머 (종료 시각 기준 계산 · 백그라운드 탭에서도 정확) | src/components/PomodoroWidget.tsx |
| PDF 내보내기 (html2canvas + jsPDF) | src/App.tsx, src/components/PdfExportTemplate.tsx |
| ICS 내보내기 (RFC 5545 이스케이프·75옥텟 폴딩·CRLF 준수) | src/App.tsx |
| Gemini 프록시 서버 함수 (키 은닉·프롬프트 서버 조립·입력 제한·빈도 제한) | api/gemini.ts |
| 테마 5색 전환 (CSS 변수 교체) | src/index.css |
| 과목 식별 색 — 명도·채도·색각이상 분리 검증을 통과한 고정 8색 | src/lib/palette.ts, src/index.css |
| 소요 시간 파싱·포맷 공용 유틸 | src/lib/duration.ts |
모션 시스템 (토큰 기반 진입·전환·순차 등장, 접근성 reducedMotion 대응) |
src/lib/motion.ts, src/components/motion/Primitives.tsx |
| 익명 기기 ID 기반 자동 저장 | src/App.tsx |
투명하게 밝혀둡니다.
- 서버는 Gemini 프록시 하나뿐입니다. 일정 생성·저장 등 나머지 로직은 전부 브라우저에서 돕니다.
- 로그인 없음.
crypto.randomUUID()로 만든 기기 ID를localStorage에 저장해 식별합니다. 브라우저를 바꾸면 데이터를 이어받을 수 없습니다. - 실제 공부한 시간은 측정하지 않습니다. 완료를 누른 시각(
completedAt)은 기록되어 연속 학습일 계산에 쓰이지만, 타이머로 실측한 시간은 아닙니다. 통계의 "실제 학습 시간"은 완료 체크된 일정의 계획 시간 합입니다. - 플랜 이력이 남지 않습니다. 저장이
user_id1행 통째 덮어쓰기라 지난 플랜은 사라집니다. - AI가 참고 링크를 만들지 않습니다. 검색 그라운딩이 유료 전용이라, 검증되지 않은 링크를 내보내는 대신 기능을 빼고 수동 입력만 남겼습니다.
- 테스트 코드 없음.
React 19 · TypeScript · Vite 6 · Zustand · Tailwind CSS 4 · Motion 12 · date-fns · lucide-react
@google/genai (gemini-3-flash-preview) · @supabase/supabase-js · jsPDF · html2canvas
| 리소스 | 출처 | 라이선스 |
|---|---|---|
| Pretendard | jsDelivr CDN | SIL OFL 1.1 |
| Inter, JetBrains Mono | Google Fonts | SIL OFL 1.1 |
| 아이콘 | lucide-react | ISC |
뽀모도로 알림음은 Web Audio API로 직접 합성합니다. 외부 음원 파일을 쓰지 않습니다.
문제 지문·이미지 등 제3자 저작 콘텐츠는 저장소에 포함되어 있지 않습니다. 설정 화면의 시험·자격증 명칭은 명칭만 사용하며 해당 기관과 제휴 관계가 없습니다.