TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
Landed: React·Express·MySQL 기반 풀스택 앱, 지원부터 오퍼까지 구직 추적
Landed — track your job search from applied to offer. Full-stack app: React frontend, Express/MySQL REST API with JWT auth, built to learn every layer.
생산성JavaScript · CSSGitHub @opki45온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Landed is a full-stack web app for tracking job applications through their lifecycle — applied, interviewing, offer, rejected, accepted. I built it end to end to turn a scattered job search into one clean, organised dashboard.README 전문 보기 ↗Landed is a full-stack web app for tracking job applications through their lifecycle — applied, interviewing, offer, rejected, accepted. I built it end to end to turn a scattered job search into one clean, organised dashboard.
Live demo: https://job-application-tracker-two-hazel.vercel.app/login
| Layer | Tech |
|---|---|
| Frontend | React (Vite), React Router, plain CSS |
| Backend | Node.js, Express, JWT auth |
| Database | MySQL (raw SQL via mysql2, no ORM) |
| Testing | Jest + Supertest (backend integration tests) |
This is a monorepo: the Express API lives in server/, the React web app in client/, and a React Native companion app (Expo Go) lives in mobile/ — see mobile/README.md. The mobile app is the same backend, same account, no server changes needed; it covers login, the home dashboard, the full applications list and detail view, the Gmail review queue, Calendar, and Analytics (Reminders and full Settings are web-only for now).
Add screenshots to a docs/ folder and reference them here, e.g.:


The backend is layered — routes → controllers → models — so responsibilities stay separated: routes map URLs to handlers, controllers hold request/response logic, and the model layer is the only place that writes SQL. The frontend talks to the API through a small fetch wrapper and a Vite dev proxy, so in development there's no CORS to configure.
server/ Express API
src/
app.js builds the app (no listen — testable)
server.js starts the HTTP server
db/ mysql2 pool + schema.sql
middleware/ JWT auth, central error handler
models/ all SQL lives here (user-scoped)
controllers/ auth / applications CRUD+pagination / Gmail integration + sync / reminders
routes/ auth + application + integration + sync + candidate + reminder routes
integrations/ Gmail/Google API boundary modules (mocked in tests)
utils/ request validation, token encryption, email prefilter
tests/ Jest + Supertest integration tests
client/ React app (Vite)
src/
api.js fetch wrapper (attaches JWT, parses errors)
AuthContext.jsx auth state + token persistence
GmailContext.jsx Gmail connection status, shared across pages
ProtectedRoute.jsx redirects if not logged in
pages/ Login, Register, Dashboard, Applications, Calendar, Analytics, Reminders, Settings
components/ AppShell (topbar+sidebar shell), Sidebar, GmailConnect, ReviewQueue,
ApplicationItem, CompanyLogo, Logo, AuthLayout, ProductPreview
mobile/ React Native app (Expo Router) — same backend, no server changes needed
app/ file-based routes: login, register, (app) group (dashboard,
applications + detail, review queue, calendar, analytics, more)
src/ api.js, AuthContext, GmailContext, theme, shared components + charts
WHERE user_id = ?, with the user id taken from the verified token, never the request. Accessing another user's row returns 404.app and server split — the Express app is built and exported without listening, so tests drive it in memory with Supertest and no open port.job_tracker_test (loaded automatically via .env.test when NODE_ENV=test), so they never touch real data.cd server
npm install
# create both databases and load the tables
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS job_tracker CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE DATABASE IF NOT EXISTS job_tracker_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p job_tracker < src/db/schema.sql
mysql -u root -p job_tracker_test < src/db/schema.sql
# configure environment
cp .env.example .env # then edit with your MySQL password + a JWT secret
cp .env .env.test # then set DB_NAME=job_tracker_test
npm run dev # API on http://localhost:3000
In a second terminal:
cd client
npm install
npm run dev # app on http://localhost:5173
Open http://localhost:5173, create an account, and start tracking. The Vite dev server proxies /api to the backend on port 3000.
cd server
npm test # runs the backend integration suite
More detail on the API and backend design is in server/README.md.
Full build spec: docs/PHASE2.md. Connect Gmail (read-only), auto-detect application-related emails, extract structured data with an LLM, and surface it in a review queue — nothing is written to applications without the user approving it. All five build-order steps are done and verified end to end against a real Gmail inbox, real applications, and both real LLM providers:
GmailConnect on the dashboard).POST /api/sync/gmail lists recent mail, dedupes against already-seen messages, and prefilters for likely-job-related content before anything reaches the LLM.src/llm/extractApplication.js, one adapter behind LLM_PROVIDER=ollama|gemini.GET /api/candidates, accept/dismiss, edit-then-approve UI. Accepted candidates write to applications through the existing model, tagged source='email'.applied < interviewing < offer/rejected < accepted) → status-update candidate, and accepting it advances the existing application instead of creating a duplicate. Matches but isn't a forward move (same stage restated) → nothing proposed.LLM provider note: the code supports both ollama (local) and gemini (cloud) behind one LLM_PROVIDER switch, per docs/PHASE2.md. In practice, local Ollama inference was too unreliable on this machine to build against (a 3B model timed out / hung mid-generation on a real sync run), so Gemini (gemini-flash-latest, free tier) is the provider actually in use for now. Ollama's adapter, config, and tests are all still there — flipping LLM_PROVIDER=ollama in .env switches back with no code changes.
Known gaps / next hardening pass: no pagination on GET /api/candidates; no rate limit on POST /api/sync/gmail (a user could trigger repeated syncs back to back); company/role matching is exact-after-normalization, not fuzzy, so a genuinely different-looking company name for the same employer won't reconcile.
The dashboard's sidebar originally had nav items with nowhere to go (Applications, Calendar, Analytics, Reminders, Settings — decorative, by explicit choice at the time). All five are now real pages:
GET /api/applications?page=), not just the dashboard's unpaginated summary table.date_applied, colour-coded by status. No new backend — derived from data already there.ON DELETE SET NULL if that application is later deleted, so the reminder survives).GmailContext instead of re-fetched per page), change password, delete account (password + typed "DELETE" confirmation; cascades via the existing ON DELETE CASCADE foreign keys), and a real light/dark/system theme picker (see below — it's not decorative anymore).A React Native companion app lives in mobile/, UI-matched to designs/mobile-view.png: login/register, a home dashboard, the full applications list, a dedicated application detail screen, the Gmail review queue, a Calendar month view, and an Analytics screen — all against the same backend with no server changes. Development is on hold as of this point — it hit real on-device bugs during testing that weren't chased down, and the whole effort is deferred to a later phase rather than iterated on further right now. The code stays in the repo (nothing was deleted); mobile/PLAN.md has the full build history if/when work picks back up. Treat anything mobile-related as unverified until then, even where earlier commits claimed something was fixed.
Real, not decorative: Settings' Light/Dark/System picker and the sidebar's quick toggle both drive one ThemeContext, persisted to localStorage and applied via a data-theme attribute on <html> (System instead relies purely on a prefers-color-scheme media query, so it also tracks a live OS-preference change without a reload). Every color in index.css runs through CSS custom properties — the light palette was already mostly tokenized, but ~99 hardcoded hex values (mostly repeated 2-8x each: the 5 status colors' pill/row tints, a success green, a danger-tint family, a purple brand-tint family, an amber warning-tint family, a few near-white hover greys) got consolidated into named tokens first, so the dark palette is a real second definition of the same design system, not a patchwork of overrides.