Collect Korea G2B bid notices, review matches, export Excel files, and send email reports from one self-hosted console.
Live Site · Features · Quick Start · Deployment
G2B Bid Report is a self-hosted web console for collecting Korea G2B public procurement notices by user-defined keywords, reviewing matched results, exporting Excel files, and sending email reports.
The project is built as a small operational MVP: one app, one database, explicit environment variables, and simple deployment primitives.
Production is published at:
https://bca.ai.kr/g2breport/
The root route redirects to /g2breport/login. Public trust and legal surfaces are available without exposing runtime secrets, local database files, or deployment credentials.
| Public URL | Purpose |
|---|---|
/g2breport/login |
Operator login and first account bootstrap |
/g2breport/privacy |
Korean privacy notice for the internal console |
/g2breport/terms |
Service notice, contact path, license, and responsibility limits |
/g2breport/api/health |
Basic database health check |
Current production smoke target:
curl https://bca.ai.kr/g2breport/api/healthLast verified production deployment:
- Event date:
2026-06-01 KST - Git commit:
7e627c099a16df740c270ce95f0af442f52de0ad - Runtime: AWS EC2 + PM2 process
g2b-bid-report - OAuth start routes: Google, Naver, and Kakao return
302redirects from production - Runtime base URL:
https://bca.ai.kr/g2breport
- Next.js App Router web console
- Email/password authentication
- Email verification and Google, Naver, Kakao social login
- User-specific include and exclude keyword rules
- User-specific recipients and collection/send schedules
- User-level automation on/off plus a server-wide scheduler switch
- Official G2B bid notice Open API collection
- Result filtering by date, mail status, keyword, and free-text query
- Excel export for collected results
- SMTP daily email report sending with send history
- Password reset, password change, and account withdrawal
- SQLite + Prisma data layer
- Light and Dracula-style dark themes with icon-only toggle controls
- In-app operator manual at
/g2breport/manual - Legal footer with privacy, service notice, MIT license, and GitHub contact links
- Security defaults for rate-limited auth, explicit role checks, hardened cookies, crawler blocking, noindex headers, and spreadsheet formula injection defense
This repository is intended to be safe to share publicly when runtime files stay out of git.
Do not commit:
.envor any real environment file- Real API keys, SMTP credentials, auth secrets, or job tokens
- Local SQLite databases such as
dev.db - Private SSH keys, PEM files, or deployment credentials
- Production hostnames, IP addresses, or server-specific paths unless they are intentionally public
Use .env.example as the public template and keep real values only in the runtime environment.
- Next.js 16
- React 19
- TypeScript
- Prisma 7
- SQLite via
better-sqlite3 bcryptjsfor password hashingjosefor signed session cookiesnodemailerfor email delivery- Built-in XLSX generator for spreadsheet export
node-cronfor optional in-app scheduling
| Route | Purpose |
|---|---|
/g2breport/login |
Login, first account creation, password reset request |
/g2breport/reset-password |
Password reset by token |
/g2breport/settings |
Keywords, recipients, schedule, and account management |
/g2breport/results |
Manual collection, filters, exports, mail send, status overview |
/g2breport/manual |
Operator workflow manual |
/g2breport/privacy |
Privacy notice |
/g2breport/terms |
Service notice, contact, and license information |
/g2breport/api/health |
Database health check |
/g2breport/api/collection/start |
Authenticated manual collection start endpoint |
/g2breport/api/collection/status |
Authenticated manual collection progress endpoint |
/g2breport/api/collection/cancel |
Authenticated manual collection cancel endpoint |
/g2breport/api/jobs/collect |
Authenticated external collection job endpoint |
/g2breport/api/jobs/send |
Authenticated external mail job endpoint |
/g2breport/api/mobile/auth/login |
Mobile app email/password login endpoint |
/g2breport/api/mobile/dashboard |
Mobile app dashboard summary endpoint |
/g2breport/api/mobile/collection/start |
Mobile app manual collection start endpoint |
/g2breport/api/mobile/reports/send |
Mobile app daily report send endpoint |
The native mobile app lives in the separate G2B-BID-REPORT-MOBILE repository,
but these /g2breport/api/mobile/* routes are owned and deployed by this server app.
The mobile dashboard follows the same result visibility policy as the web console:
repeat-matched existing notices refresh their confirmation time, appear in the
todayConfirmed metric, and expose confirmedAt while keeping collectedAt
for compatibility.
- Login, registration, OAuth start, email verification, account lookup, password reset, password change, and withdrawal attempts are rate-limited.
- User sessions are signed cookies with
HttpOnly, productionSecure, andSameSite=Lax. - Application roles are explicitly constrained to
adminanduserbefore protected actions proceed. - Passwords are stored only as bcrypt salted hashes; new or changed passwords must satisfy the strong password policy.
- Sensitive session/auth data is not stored in
localStorage; only UI preferences such as theme/sidebar state use browser storage. - Database access uses Prisma ORM bindings; do not add concatenated raw SQL.
- Excel export neutralizes spreadsheet formula injection for values starting with
=,+,-,@, or control line prefixes. - Public crawler exposure is blocked with
/robots.txt,X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, noimageindex, and known search/AI crawler UA blocking in the proxy. - Production health/error responses avoid stack traces, debug payloads, framework banners, and exact server version disclosure.
- nginx deployments must keep
server_tokens off;and custom error pages that preserve the original status code.
cp .env.example .env
npm install
npm run db:migrate
npm run devOpen the local app:
http://localhost:3000/g2breport
On a fresh database, the login page prompts for the first operator account.
Copy .env.example to .env and fill in local or production values.
| Variable | Description |
|---|---|
DATABASE_URL |
SQLite database URL, usually file:./dev.db for local development |
AUTH_SECRET |
Long random secret used to sign session cookies |
AUTH_COOKIE_SECURE |
Set true for HTTPS deployments, false for plain HTTP local/dev use |
SMTP_HOST |
SMTP server host |
SMTP_PORT |
SMTP server port |
SMTP_USER |
SMTP username |
SMTP_PASS |
SMTP password or app password |
MAIL_FROM |
Email sender address, optionally with display name such as G2B-Report <bot@example.com> |
GOOGLE_OAUTH_CLIENT_ID |
Google OAuth web client ID for Google login |
GOOGLE_OAUTH_CLIENT_SECRET |
Google OAuth web client secret |
NAVER_OAUTH_CLIENT_ID |
Naver Developers client ID for Naver login |
NAVER_OAUTH_CLIENT_SECRET |
Naver Developers client secret |
KAKAO_REST_API_KEY |
Kakao Developers REST API key for Kakao login |
KAKAO_CLIENT_SECRET |
Optional Kakao client secret, only when enabled in Kakao Developers |
G2B_API_SERVICE_KEY |
Official G2B Open API service key |
G2B_API_LOOKBACK_DAYS |
Number of registration days to search backwards |
G2B_API_NUM_ROWS |
Rows per API page |
G2B_API_MAX_PAGES_PER_ENDPOINT |
Maximum pages to request per endpoint |
G2B_API_CONCURRENCY |
Concurrent API request limit |
ENABLE_INTERNAL_SCHEDULER |
Server-wide in-app scheduler switch |
INTERNAL_JOB_TOKEN |
Bearer token for external job endpoints |
APP_BASE_URL |
Public base URL used by OAuth callbacks, job scripts, and password reset links |
If SMTP variables are empty, email sending is skipped and recorded as a skipped mail history entry.
For Gmail SMTP, use an app password instead of the Google account password:
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="465"
SMTP_USER="your-account@gmail.com"
SMTP_PASS="your-16-character-app-password"
MAIL_FROM="G2B-Report <your-account@gmail.com>"Social login redirect URLs:
https://bca.ai.kr/g2breport/api/auth/oauth/google/callback
https://bca.ai.kr/g2breport/api/auth/oauth/naver/callback
https://bca.ai.kr/g2breport/api/auth/oauth/kakao/callback
Production OAuth callbacks are pinned to the public site URL. If a production
runtime accidentally receives a localhost base URL, the auth flow falls back to
https://bca.ai.kr/g2breport instead of generating localhost redirects.
Social login requests and stores only the email address needed to identify the account. Do not enable profile/name/nickname permissions in provider consoles unless the product explicitly needs them later.
Collection runs per user.
- At least one active include keyword is required.
- The app queries the official G2B bid notice Open API.
- A notice must include today's KST date between its notice date and close date.
- Include keywords are matched against notice title and organization fields.
- Exclude keywords remove otherwise matched notices.
BidNoticeis upserted by notice number and order.CollectedResultis unique per user and notice.- If an existing user result is matched again, its confirmation time and matched keyword are refreshed so it remains visible in today's default result list.
- API failures do not create partial result rows.
KST date bounds:
noticeDate <= today 23:59:59.999 KST
closeDate >= today 00:00:00.000 KST
Automation has two layers:
ENABLE_INTERNAL_SCHEDULER: server-wide on/off switchScheduleSetting.active: per-user on/off switch managed in the UI
The effective status is active only when both are on. External job endpoints also process active schedules only.
Send jobs use each user's saved ScheduleSetting.sendTime and timezone to build
the daily report window from the previous send time to the current send time.
The report includes every notice confirmed in that window, including existing
notices that matched again and were refreshed into the current result list.
Daily report duplicate checks are recipient-scoped. A recipient that already
has a successful send history for the same report window is skipped, while
failed, skipped, or newly activated recipients remain eligible for retry.
npm run dev # Start local development server
npm run test # Run ESLint and TypeScript checks
npm run build # Generate Prisma Client and build Next.js
npm run start # Start production Next.js server
npm run db:generate # Generate Prisma Client
npm run db:migrate # Apply Prisma migrations
npm run db:push # Push schema directly to the database
npm run job:collect # Call the external collect job endpoint
npm run job:send # Call the external send job endpointExternal schedulers can trigger collection or sending:
POST /g2breport/api/jobs/collect
POST /g2breport/api/jobs/send
Required headers:
Authorization: Bearer <INTERNAL_JOB_TOKEN>
Content-Type: application/json
Run for all active users:
{}Run for one user:
{
"userId": "user-id"
}Basic database check:
curl http://localhost:3000/g2breport/api/healthExample response:
{
"ok": true,
"database": "connected",
"checkedAt": "2026-05-20T00:00:00.000Z"
}Detailed health data requires the internal job token:
curl "http://localhost:3000/g2breport/api/health?detailed=1" \
-H "Authorization: Bearer <INTERNAL_JOB_TOKEN>"- Prepare runtime environment variables outside git.
- Install dependencies.
- Apply migrations.
- Run tests.
- Build the app.
- Start or restart the process manager.
- Check
/g2breport/api/health.
Example:
npm install
npm run db:migrate
npm run test
npm run build
npm run startThe current production server deploys from the GitHub main branch.
git fetch origin
git pull --ff-only origin main
npm run build
pm2 restart g2b-bid-report --update-env
curl http://localhost:3000/g2breport/api/healthPublic post-deploy smoke check:
curl https://bca.ai.kr/g2breport/api/health
curl -I https://bca.ai.kr/g2breport/
curl -I https://bca.ai.kr/g2breport/api/auth/oauth/google/start
curl -I https://bca.ai.kr/g2breport/api/auth/oauth/naver/start
curl -I https://bca.ai.kr/g2breport/api/auth/oauth/kakao/startExpected behavior:
/g2breport/api/healthreturns{"ok":true,"database":"connected",...}/redirects to/g2breport/login- OAuth start routes return
302to each provider with the production callback URL - The PM2 process
g2b-bid-reportisonline
Recent production events:
| Date (KST) | Commit | Event | Verification |
|---|---|---|---|
| 2026-06-01 12:56 | f5404c9 |
Deployed the mobile auth/API routes needed by the native app, including mobile registration, social login start/callback, collection status, and uncapped dashboard results. | Local lint/typecheck/build passed, localhost mobile social API smoke passed for Google/Naver/Kakao, production build passed, PM2 online, /api/health OK, production mobile social API smoke passed for Google/Naver/Kakao, Android Samsung/iPhone/iPad Maestro smoke passed. |
| 2026-06-01 05:27 | 945ce86 |
Aligned the mobile dashboard API with web repeat-collection visibility by exposing todayConfirmed, confirmedAt, and collection policy metadata while preserving collectedAt. |
Local lint/typecheck/build passed, production build passed, PM2 online, /api/health OK, /login HTTP 200, unauthenticated mobile dashboard returned 401, authenticated dashboard smoke returned todayConfirmed=322 and confirmedAt. |
| 2026-06-01 05:17 | 55f7a4f |
Refreshed already-saved matching notices during repeat collection so open notices remain visible in today's default result list without duplicate rows or duplicate mail. | Local lint/typecheck/build passed, production build passed, PM2 online, /api/health OK, /login HTTP 200. |
| 2026-06-01 05:05 | 03c712c |
Restored functional section boundaries for metrics, manual actions, automation status, and nested operational groups. | Local lint/build passed, Playwright web screenshot confirmed section borders, production build passed, PM2 online, /api/health OK, /login HTTP 200. |
| 2026-06-01 04:59 | 64c7a35 |
Restored visible boundaries for bid result tables while keeping non-data header chrome flat. | Local lint/build passed, Playwright web screenshot captured with 306 result rows, production build passed, PM2 online, /api/health OK, /login HTTP 200. |
| 2026-06-01 04:49 | 9ba5403 |
Flattened the web UI by removing unnecessary card, box, and shadow treatment from operational screens. | Local lint/build passed, production build passed, PM2 online, /api/health OK, /login HTTP 200. |
- Confirm
G2B_API_SERVICE_KEYis configured. - Confirm at least one include keyword is active.
- Confirm the target notice is within the configured lookback window.
- Confirm today's KST date is between the notice date and close date.
- Check whether an exclude keyword filtered the result.
- Use a stable
AUTH_SECRET. - Use
AUTH_COOKIE_SECURE=truebehind HTTPS. - Use
AUTH_COOKIE_SECURE=falseonly for local/plain HTTP environments.
- Confirm SMTP variables are configured.
- Confirm the sender account allows SMTP login.
- For Gmail, confirm a Google app password is used and
SMTP_PASScontains the compact 16-character password. - Confirm
MAIL_FROMis either a plain address or a valid display-name format such asG2B-Report <account@gmail.com>. - Empty SMTP settings intentionally create skipped mail history instead of sending.
- Daily reports are skipped when there are no notices confirmed between the previous configured send time and the current configured send time.
- A successfully sent recipient is not sent again for the same report date, but failed, skipped, or newly activated recipients can receive the same daily report on retry.
MIT