landing/: Django application code rootlanding/project/: Django project settings/urls/asgi/wsgilanding/templates/: HTML templateslanding/static/: CSS/JS static assetsimages/: image assets used by pagesopenspec/: OpenSpec changes/spec historymanage.py: local entrypoint
quroom_landing/->landing/project/templates/->landing/templates/static/->landing/static/
- All landing / vibe-coding docs have moved to
openspec/changes/consolidate-codex-docs-into-openspec/docs/. codex-document/now only contains a pointer README.
- Landing spec:
openspec/changes/consolidate-codex-docs-into-openspec/docs/quroom-landing-spec.md - Dokku deploy runbook:
codex-document/dokku-deploy-runbook.md - Production DB backup runbook:
codex-document/db-backup-runbook.md - Search engine registration runbook:
codex-document/search-engine-registration-runbook.md - AX tool stack + diagnosis question source:
landing/ax_tool_stack.py - Vibe coding guides & prompt library:
openspec/changes/consolidate-codex-docs-into-openspec/docs/general-vibe-coding-guide-for-beginners.mdopenspec/changes/consolidate-codex-docs-into-openspec/docs/vibe-coding-step-by-step-guide.mdopenspec/changes/consolidate-codex-docs-into-openspec/docs/vibe-coding-prompt-library.md
- Asset paths remain under
images/(unchanged). - If you reintroduce
quroom-landing-openspec.md, place it under the consolidated docs path.
- Install dependencies
./scripts/setup-dev.shsource .venv/bin/activate
- Run server
python manage.py migratepython manage.py runserver
- Open
http://127.0.0.1:8000/- Free diagnosis page:
http://127.0.0.1:8000/free-diagnosis/
Prerequisites:
- Docker / Docker Compose
- a downloaded landing dump, or SSH access to fetch it with
./scripts/fetch-prod-db-backup.sh
- Fetch and restore the latest landing backup into Docker Compose Postgres
./scripts/up-landing-snapshot-compose.sh --fetch-latest- first run creates a gitignored
.env.landing-snapshotwith a local-only random DB password
- Start Django in Docker Compose against that restored snapshot
./scripts/run-landing-snapshot-local.sh
- Open
http://127.0.0.1:8011/
- Stop
./scripts/down-landing-snapshot-compose.sh
This flow does not require a local .venv. The snapshot app image is built from Dockerfile.landing-snapshot using requirements.txt.
- Django system check:
./scripts/django-check.sh
- Direct equivalent:
.venv/bin/python manage.py check
Use this command as the default before applying or archiving OpenSpec changes.
- System check
./scripts/format-check.sh
- Django system check
./scripts/django-check.sh
- Django test suite
./scripts/django-test.sh
- One-shot verification
./scripts/verify.sh
If both pass, push to GitHub.
- Workflow:
.github/workflows/deploy-dokku.yml - Trigger:
mainbranch push (or manual dispatch) - Required secret:
DOKKU_SSH_PRIVATE_KEY(dokku push 권한이 있는 개인키)
- Roll back to previous deployed commit + smoke checks:
./scripts/dokku-rollback.sh
- Roll back to explicit commit SHA:
./scripts/dokku-rollback.sh --target-sha <sha>
- Python format/lint:
- apply:
./scripts/format-apply.sh - check:
./scripts/format-check.sh
- apply:
- Django template format:
djlintis the canonical formatter forlanding/templates/**- avoid formatting template files with generic HTML formatters
- VS Code workspace defaults:
- Python: Ruff formatter
- Django template (
django-html): djLint formatter - generic HTML remains separate
- If a specific line must keep manual layout, use djLint ignore pragmas around the minimum block.
- Prefer splitting long inline template expressions across tags/elements instead of relying on formatter behavior.
- Tailwind utility classes are the default choice for page-level layout, spacing, color, borders, typography sizing, and one-off visual adjustments inside templates.
landing/static/landing/css/site.cssis reserved for:- global element defaults (
body, form fields, anchors/offset behavior) - reusable project-specific classes not worth expressing repeatedly in templates
- font-family hooks like
.font-space - small custom animations/utilities such as
.loading-bar
- global element defaults (
- Do not duplicate the same concern in both places.
- Example: if spacing/color/border can be expressed with template utility classes, keep it in Tailwind classes.
- Example: if a behavior is global across many templates, keep it in
site.css.
- Tailwind is loaded from CDN and extended in base.html with single custom color tokens:
navyskyfogmintamber
- Because
amberis defined as a single custom token, shade classes likebg-amber-100orborder-amber-300are not reliable in this project. - Use these forms instead:
bg-amberbg-amber/10border-amber/30text-amber-700only when relying on Tailwind default palette classes already available
- When in doubt, prefer the custom-token style (
bg-amber/...,border-amber/...) for project-specific colors.
- In network-restricted environments,
ruff/djlintinstallation can fail until package index access is available. - Once dependencies are available, run
./scripts/format-apply.shthen./scripts/verify.shto establish baseline formatting in changed files.
- Prerequisite:
- GNU gettext binaries (
msguniq,msgfmt) must be installed on your OS. - macOS:
brew install gettext(then ensure PATH includes gettext bin) - Ubuntu/Debian:
sudo apt-get install gettext
- GNU gettext binaries (
- Generate catalogs:
.venv/bin/python manage.py makemessages -l ko -l en
- Compile catalogs:
.venv/bin/python manage.py compilemessages
- Notes:
- Keep user-facing strings in templates/forms/views under
{% trans %}/gettext(_lazy)somakemessagescan extract them. - After translation edits, always run
compilemessagesand./scripts/verify.sh.
- Keep user-facing strings in templates/forms/views under
DJANGO_SECRET_KEYDJANGO_DEBUG(1or0)DJANGO_ALLOWED_HOSTS(comma-separated)DATABASE_URL(preferred in production)PGHOST,PGPORT,PGDATABASE,PGUSER,PGPASSWORD(fallback if noDATABASE_URL)DB_HOST,DB_PORT,DB_NAME,DB_USER,DB_PASSWORD(Cloudtype-style fallback keys)GA4_MEASUREMENT_ID(if you want GA4 tracking)ANALYTICS_EXCLUDED_IPS(comma-separated IPs to exclude from FunnelEvent tracking, e.g.203.0.113.10,198.51.100.7)QUROOM_CONTACT_EMAIL(defaults to[email protected])CONTACT_EMAIL_ASYNC(1enables async email send, default0)DJANGO_SITE_BASE_URL(메일 CTA 링크 기준 URL, 예:https://quroom.kr)SEARCH_ROBOTS_EXTRA_LINES(robots.txt하단 추가 라인. 여러 줄 입력 가능.User-agent,Allow,Disallow,Sitemap,#주석만 노출)
- Canonical base URL:
https://quroom.kr(운영 기준) - Site endpoints:
https://quroom.kr/robots.txthttps://quroom.kr/sitemap.xml
- Operator runbook:
codex-document/search-engine-registration-runbook.md
운영 시 DJANGO_SITE_BASE_URL은 canonical 기준 URL과 동일하게 유지하고, 검색엔진 요청에 따라 SEARCH_ROBOTS_EXTRA_LINES로 robots.txt 하단 문구를 추가합니다. Content-Signal처럼 robots 표준 지시어가 아닌 라인은 Lighthouse와 일부 크롤러에서 Unknown directive로 잡히므로 응답에서 제외됩니다.
- Build/install dependencies from
requirements.txt. - Set production env values:
DJANGO_DEBUG=0DJANGO_SECRET_KEY(non-default)DJANGO_ALLOWED_HOSTS(no*)DJANGO_CSRF_TRUSTED_ORIGINSDATABASE_URL(or PG*/DB* variables)
- Use release command:
python manage.py migrate --no-input && python manage.py collectstatic --no-input
- Use web command:
gunicorn --workers=2 --threads=2 --bind 0.0.0.0:${PORT:-8000} landing.project.wsgi:application
Procfile already contains matching release/web entries.
.env.local: daily local development profile (safe mode)DJANGO_DEBUG=1DJANGO_EMAIL_BACKEND=consoleDJANGO_ALLOW_REAL_EMAIL_IN_DEBUG=0
.env.debug: intentional debug profile for real SMTP testsDJANGO_DEBUG=1DJANGO_EMAIL_BACKEND=smtpDJANGO_ALLOW_REAL_EMAIL_IN_DEBUG=1
.env.deploy: production deployment templateDJANGO_DEBUG=0- real SMTP backend + async enabled
- domain/CSRF/secret must be replaced before use
- Production should NOT use local files; set variables in Railway.
- Apply local safe profile:
cp .env.local .env
- Apply debug SMTP test profile:
cp .env.debug .env
- Create deploy template copy (local dry run only):
cp .env.deploy .env
- (optional) load variables into current shell:
set -a; source .env; set +a
DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackendEMAIL_HOST(example:smtp.daum.net)EMAIL_PORT(example:465for SSL or587for TLS)EMAIL_HOST_USEREMAIL_HOST_PASSWORDEMAIL_USE_TLS(1for TLS/587)EMAIL_USE_SSL(1for SSL/465)EMAIL_TIMEOUT(seconds, default20)DJANGO_DEFAULT_FROM_EMAIL(recommended to matchEMAIL_HOST_USER)
- Contact form endpoint:
/contact/submit/ - Lead magnet endpoint:
/lead-magnet/submit/ - Lead magnet page:
/free-diagnosis/ - Lead magnet flow:
- user submits 8-question diagnosis
- partial result is shown immediately (score/grade/top priorities)
- full detailed report is delivered by email
- Enable:
CONTACT_EMAIL_ASYNC=1
- Behavior:
- form response returns immediately
- email sending runs in background thread
- UI shows "처리 중" loading indicator during HTMX request
- Note:
- current async mode is process-local thread based (simple, no queue persistence)
- if you need retry/durable jobs, migrate to Celery/Redis later
- Production value should be:
- Verify in runtime settings before deploy:
.venv/bin/python manage.py shell -c "from django.conf import settings; print(settings.QUROOM_CONTACT_EMAIL)"
- SMTP smoke test:
set -a; source .env; set +a.venv/bin/python manage.py shell -c "from django.core.mail import send_mail; from django.conf import settings; print(send_mail('[SMTP 테스트] QuRoom','SMTP 테스트', settings.DEFAULT_FROM_EMAIL, [settings.QUROOM_CONTACT_EMAIL], fail_silently=False))"
- Django Admin:
/admin/ - Admin dashboard:
/admin-dashboard/ - Recommended quick checks:
- verify contact inquiry count increases after form submit
- verify lead-magnet inquiry appears with type
lead_magnet_diagnosis - verify funnel events (
lead_magnet_start,lead_magnet_submit,lead_magnet_email_sent) are visible in dashboard metrics - verify your current IP is shown in
유입 제외 IP 관리and can be added/deactivated from the dashboard
- Create an invite in Django Admin:
Testimonial invites(set target note and expiry). - Share URL
/testimonials/invite/<token>/right after the consultation. - Confirm submission is stored as
pendingand invite is marked consumed. - Review testimonial in Admin and change status to
approvedonly if publish-safe. - Public testimonial section appears on homepage only when approved count reaches threshold (
TESTIMONIAL_PUBLIC_THRESHOLD, default3).