Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
12f4f03
feat: refresh public experience and import goodprice data
answndud Apr 2, 2026
5b58c1f
fix: stabilize deployed header navigation
answndud Apr 2, 2026
2846e00
fix: include map repository aggregation changes
answndud Apr 2, 2026
f7bc476
fix: sync database map query changes
answndud Apr 2, 2026
fa1d3aa
feat(ui): refine mobile map sheets
answndud Apr 3, 2026
ac39e6e
docs(progress): record mobile map deploy
answndud Apr 3, 2026
8686fbc
fix(map): restore mobile place markers
answndud Apr 3, 2026
69e692e
docs(progress): record mobile marker deploy
answndud Apr 3, 2026
25971e1
fix(map): re-cluster mobile markers on zoom out
answndud Apr 3, 2026
26ec078
docs(progress): record zoom-out map deploy
answndud Apr 3, 2026
7a898e1
feat(ui): tighten public layout density
answndud Apr 3, 2026
82c8b88
fix(build): restore route reset details
answndud Apr 3, 2026
f2d416a
docs(progress): record ui deploy
answndud Apr 3, 2026
071e639
fix(map): restore wide viewport markers
answndud Apr 3, 2026
44ceee2
fix(build): restore public worker deploy
answndud Apr 3, 2026
e06463e
fix(build): restore public worker map runtime
answndud Apr 3, 2026
366ff26
feat(app): ship latest public and admin updates
answndud Apr 5, 2026
90268b2
docs(progress): record marker mode deploy
answndud Apr 5, 2026
fcb178e
feat(map): redesign category markers
answndud Apr 5, 2026
464fc00
docs(progress): record marker redesign deploy
answndud Apr 5, 2026
0f09c11
fix(map): boost marker contrast
answndud Apr 5, 2026
9164c6a
docs(progress): record marker contrast deploy
answndud Apr 5, 2026
77dd22d
fix(data): refine goodprice quota selection
answndud Apr 5, 2026
e058c42
docs(progress): record goodprice deploy
answndud Apr 5, 2026
ab7ae4f
fix(data): tighten goodprice price ceiling
answndud Apr 5, 2026
56f6ca2
docs(progress): record goodprice ceiling deploy
answndud Apr 5, 2026
36bcd4b
fix(map): soften cluster palette
answndud Apr 6, 2026
2f018dc
docs(progress): record cluster palette deploy
answndud Apr 6, 2026
a19acaa
fix: restore admin deploy and polish portfolio readme
answndud Apr 6, 2026
ee4c485
chore: clean up screenshot script logging
answndud Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 67 additions & 27 deletions PLAN.md

Large diffs are not rendered by default.

1,237 changes: 1,231 additions & 6 deletions PROGRESS.md

Large diffs are not rendered by default.

279 changes: 70 additions & 209 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,243 +1,104 @@
# 알뜰맵
# 알뜰맵 (Altteulmap)

알뜰맵은 내 주변의 저렴한 식당과 생활 서비스 정보를 지도에서 찾는 웹 서비스입니다.
> A map-first service for finding, contributing, and moderating affordable local places in Korea.

현재 프로젝트는 로컬 개발 우선으로 초기 세팅되어 있습니다.
- 앱 개발: 일반 Next.js 개발 서버 사용
- Cloudflare 대응: OpenNext/Wrangler 설정 포함
- 실제 배포: Cloudflare 계정 준비 후 진행
![Next.js](https://img.shields.io/badge/Next.js-16-111111?logo=nextdotjs&logoColor=white)
![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-f38020?logo=cloudflare&logoColor=white)
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-17-336791?logo=postgresql&logoColor=white)
![Playwright](https://img.shields.io/badge/Playwright-E2E-2EAD33?logo=playwright&logoColor=white)
![Status](https://img.shields.io/badge/Status-Live-success)

## 로컬 개발
- Demo: [altteulmap.altteul-lab.workers.dev](https://altteulmap.altteul-lab.workers.dev)
- Admin: [altteulmap-admin.altteul-lab.workers.dev](https://altteulmap-admin.altteul-lab.workers.dev)
- Docs: [Cloudflare deploy guide](docs/deploy-cloudflare.md)

```bash
npm run dev
```

브라우저에서 `http://localhost:3000`을 열면 됩니다.

로컬 개발 서버는 `webpack` 기반으로 실행되고 산출물을 `.next-dev`에 저장합니다. 반대로 `build`, `start`, Playwright E2E, Cloudflare 빌드는 계속 `.next` 또는 `.open-next`를 사용하므로, dev 서버를 띄운 채 빌드/검증을 돌려도 예전처럼 같은 `.next`를 공유하며 깨지지 않습니다.

페이지 전환이 멈추거나 dev cache가 이상하면 아래처럼 dev 산출물만 비우고 다시 올리면 됩니다.

```bash
rm -rf .next-dev
npm run dev
```

## 주요 스크립트

```bash
npm run dev
npm run lint
npm run build
npm run verify
npm run test:e2e:smoke
npm run test:e2e
npm run smoke:local
npm run deploy:check
npm run admin:build
npm run cf:build:admin
npm run hooks:install
npm run db:up
npm run db:generate
npm run db:push
npm run db:seed
npm run data:goodprice
npm run db:down
npm run preview
```

- `dev`: Next.js 로컬 개발 서버 (`webpack`, `.next-dev`)
- `lint`: ESLint 검사
- `build`: Next.js 프로덕션 빌드 (`.next`)
- `verify`: 현재 프로젝트 기준 전체 기본 검증(`lint + build`)
- `test:e2e:smoke`: `main` 푸시 기준의 빠른 Playwright smoke 세트
- `test:e2e`: Playwright E2E 실행
- `smoke:local`: 실행 중인 로컬 서버에 대해 SEO/API/credentials 로그인 기본 스모크 체크
- `deploy:check`: Cloudflare 배포 전 필수 환경 변수와 URL 설정 점검
- `admin:sync`: public 앱의 관리자 route entrypoint를 `embedded` 또는 `external` 구현으로 동기화
- `admin:build`: 별도 `apps/admin` 관리자 앱 빌드
- `cf:build:admin`: 별도 `apps/admin` 관리자 Worker용 OpenNext build
- `hooks:install`: 이 저장소 전용 git hook 활성화
- `db:up`: 로컬 Postgres 컨테이너 시작
- `db:generate`: Drizzle 마이그레이션 SQL 생성
- `db:push`: 로컬/개발 DB에 스키마 반영
- `db:seed`: 로컬 DB에 시드 데이터 입력 (`imported-goodprice.json`이 있으면 실제 착한가격업소 1000건 우선 사용)
- `data:goodprice`: 행정안전부 `착한가격업소` 사이트에서 `1만원 이하` 실제 업소를 수집해 `src/features/places/imported-goodprice.json`과 `data/goodprice/import-meta.json` 생성
- `db:down`: 로컬 Postgres 컨테이너 중지
- `cf:clean`: Cloudflare 빌드 전 `.next`, `.next-dev`, `.open-next` 정리
- `cf:build`: Cloudflare 배포용 clean build
- `preview`: OpenNext로 Cloudflare Workers 런타임 미리보기
- `preview:public`: 관리자 route를 제외한 public 앱 preview
- `preview:admin`: 별도 관리자 앱 preview
- `deploy:public`: 관리자 route를 제외한 public 앱 배포
- `deploy:admin`: 별도 관리자 앱 배포

`deploy`, `deploy:public`, `deploy:admin`, `upload`는 Cloudflare 계정과 Wrangler 인증이 준비된 뒤 사용하면 됩니다.

현재 산출물 경로는 `dev -> .next-dev`, `build/start/e2e -> .next`, `Cloudflare preview/deploy -> .open-next`로 분리돼 있습니다. Cloudflare 무료 플랜 기준 경량화는 `next build --webpack` + `cf:clean` 경로를 전제로 맞춰져 있습니다.

관리자 분리 1차가 들어가 있어서, 현재 배포 경로는 두 가지입니다.
- 기본 `deploy`: 관리자 구현을 포함한 현재 앱 전체 배포
- `deploy:public`: public 앱만 배포하고 `/admin`, `/api/admin`은 번들에서 제외하는 경로
- `deploy:admin`: `apps/admin`을 `altteulmap-admin` 같은 별도 Worker로 배포하는 경로

`deploy:public`은 `ADMIN_APP_URL`이 반드시 있어야 합니다. public 앱에서 관리자 링크를 별도 관리자 앱으로 보낼 때 쓰는 값이기 때문입니다.

별도 관리자 앱은 `apps/admin`에서 관리하며, 로컬 검증은 `npm run admin:build`, Worker 번들 검증은 `npm run cf:build:admin`으로 먼저 확인합니다.

## DB 시작

DB 없이도 앱은 바로 실행됩니다. `.env`가 없거나 `USE_MOCK_DATA=true`면 자동으로 목업 데이터를 사용합니다.

```bash
npm run db:up
cp .env.example .env
```

그 다음 `.env`에서 `USE_MOCK_DATA=false`로 바꾸고 아래 순서로 진행하면 됩니다.

```bash
npm run db:generate
npm run db:push
npm run db:seed
```

로그인과 운영자 화면까지 확인하려면 시드 데이터가 필요합니다. 기본 로컬 계정은 아래 환경 변수 조합을 사용합니다.
![Altteulmap home](docs/readme/hero-home.png)

- 일반 사용자: `demo@altteulmap.local` / `AUTH_DEMO_PASSWORD`
- 운영자: `admin@altteulmap.local` / `AUTH_ADMIN_PASSWORD`
## At a Glance

DB가 연결된 상태에서는 `/signup`에서 새 이메일 계정을 직접 만들 수 있습니다. 가입이 끝나면 같은 이메일/비밀번호로 바로 로그인됩니다.
- 실제 공공 데이터 1,000건을 정제해 지도 탐색 경험에 투입했습니다.
- 장소 등록, 댓글, 가격 제보, 신고는 공개 참여로 받고 운영자가 검수합니다.
- 공개 앱과 관리자 앱을 Cloudflare Workers로 분리해 무료 플랜 제약 안에서 배포했습니다.
- Playwright E2E, smoke, live deploy check까지 포함해 “보여주는 데모”보다 “운영 가능한 MVP”에 가깝게 만들었습니다.

실제 데이터를 다시 받으려면 아래 순서로 실행하면 됩니다.
## Highlights

```bash
npm run data:goodprice -- --delay-ms=50 --timeout-ms=10000
npm run db:seed
```
- `Map-first UX`: 첫 화면이 바로 지도이고 목록과 상세 시트가 같은 맥락에서 이어집니다.
- `Real data import`: 행정안전부 `착한가격업소` 데이터를 직접 수집·정규화·적재했습니다.
- `Open contribution loop`: 익명 제보를 허용하되 운영자 승인으로 데이터 품질을 관리합니다.
- `Public/Admin split`: 번들 크기와 운영 제약에 맞춰 public worker와 admin worker를 분리했습니다.
- `Verified delivery`: lint, build, Playwright E2E, local/remote smoke, live Cloudflare deploy까지 닫았습니다.

생성된 `src/features/places/imported-goodprice.json`은 mock fallback과 DB seed 양쪽에서 공통으로 우선 사용합니다. 수집 메타와 원본 업소 id/지역 분포는 `data/goodprice/import-meta.json`에 남습니다.
## Screenshots

기본 예시는 `.env.example`에 들어 있고, 로컬 `.env`도 같은 값으로 맞춰두었습니다.
| Home | Detail Sheet |
| --- | --- |
| ![Home](docs/readme/hero-home.png) | ![Detail Sheet](docs/readme/place-detail.png) |

`/`에서 실제 네이버 지도를 보려면 `NEXT_PUBLIC_NAVER_MAP_KEY_ID`를 설정하면 됩니다. 아직 키가 없으면 같은 화면에서 자동으로 임시 프리뷰 지도로 fallback됩니다. 기존 `NEXT_PUBLIC_NAVER_MAP_CLIENT_ID` 값도 함께 지원합니다.
| Mobile | Submission |
| --- | --- |
| ![Mobile Map Sheet](docs/readme/mobile-map-sheet.png) | ![Submission Form](docs/readme/submit-form.png) |

`NEXTAUTH_URL`은 로그인 callback뿐 아니라 `robots.txt`, `sitemap.xml`, canonical metadata의 기준 URL로도 사용합니다. 배포 시에는 반드시 실제 도메인으로 바꿔야 합니다.
| Admin |
| --- |
| ![Admin Console](docs/readme/admin-console.png) |

관리자 앱을 분리할 때는 `ADMIN_APP_URL`도 같이 설정합니다. 예를 들면 `https://altteulmap-admin.altteul-lab.workers.dev`처럼 별도 관리자 Worker 주소를 넣고, 그 뒤 public 앱을 `deploy:public`으로 배포합니다.
## How It Works

소셜 로그인을 붙일 때는 지도 키와 분리해서 아래 환경 변수를 사용합니다.
1. 지도에서 현재 위치와 검색어 기준으로 저렴한 장소를 찾습니다.
2. 상세 시트에서 가격, 반응, 공유, 북마크를 확인합니다.
3. 비회원도 장소 등록, 댓글, 가격 제보, 신고를 남길 수 있습니다.
4. 운영자가 별도 관리자 앱에서 검토 후 공개 지도에 반영합니다.

- `AUTH_KAKAO_CLIENT_ID`
- `AUTH_KAKAO_CLIENT_SECRET`
- `AUTH_NAVER_CLIENT_ID`
- `AUTH_NAVER_CLIENT_SECRET`
## Why This Repo Is Worth Opening

OAuth callback URL은 기본적으로 아래 경로를 사용합니다.
- `서비스 관점`: 탐색, 참여, 검수, 반영까지 한 사이클이 실제로 닫혀 있습니다.
- `엔지니어링 관점`: Cloudflare Workers 무료 플랜 제약 때문에 public/admin split, 번들 경량화, runtime smoke를 직접 정리했습니다.
- `데이터 관점`: mock이 아니라 실데이터 import와 운영용 moderation 흐름이 같이 있습니다.
- `작업 방식 관점`: AI를 구현 보조로만 쓰지 않고 계획, 검증, 문서화 루프까지 통제했습니다.

- 카카오: `/api/auth/callback/kakao`
- 네이버: `/api/auth/callback/naver`
## AI-Native Workflow

## 로컬 AI 워크플로우
- `PLAN.md`와 `PROGRESS.md`를 유지하며 계획과 실행 로그를 분리했습니다.
- 구현 후에는 `lint`, `build`, `Playwright E2E`, `smoke`를 기준으로 종료했습니다.
- repo-local hooks와 검증 스크립트로 품질 기준을 저장소 안에 고정했습니다.

이 저장소에는 전역 설정 대신 repo-local AI 워크플로우 파일이 들어 있습니다.
## Verification

```bash
npm run hooks:install
npm run verify
```

- `.agents/skills/`: API, migration, 검증, E2E 기준
- `.agents/reviewers/`: TypeScript/DB 자체 리뷰 체크리스트
- `.githooks/`: 이 저장소 전용 pre-commit, commit-msg hook

hook를 설치하면 아래가 자동으로 검사됩니다.
- staged 코드의 `debugger`/secret 패턴
- `npm run verify:quick`
- conventional commit 메시지 형식
- lint/formatter 설정 파일 수정 차단

로컬 서버가 이미 떠 있다면 아래로 빠른 런타임 점검을 할 수 있습니다.

```bash
npm run smoke:local
```

기본 대상은 `http://localhost:3000`이고, 다른 포트에서 확인하려면 `SMOKE_BASE_URL=http://localhost:3102 npm run smoke:local`처럼 실행하면 됩니다.

Playwright E2E는 아래 명령으로 실행합니다.

```bash
npm run playwright:install
npm run test:e2e:smoke
npm run test:e2e
npm run smoke:local
npm run smoke:remote
```

현재 E2E는 로컬 안정성을 위해 빌드 후 그룹별로 나뉩니다.
- `test:e2e:smoke`: `map`, `admin-dashboard`, `signup`, `submission-admin`
- `test:e2e`: smoke + `map.mobile` + `bookmarks`, `comments`, `price-review`, `report-admin`

로컬 E2E 명령은 `.env.production.local`이 있어도 `.env`와 `.env.local` 값을 우선 주입해 local DB와 local auth 기준으로 실행합니다. 이때 `NEXTAUTH_URL`은 Playwright 서버 포트에 맞춰 `http://127.0.0.1:3107`로 고정되고, DB 기반 세트는 `db:push -> db:seed`를 먼저 실행합니다. CI는 반대로 workflow env를 명시적으로 주입합니다.

현재 기본 E2E는 아래 흐름을 검증합니다.
- 지도 첫 진입
- 검색과 상세 시트 열기/닫기
- 모바일 목록 시트 열기/닫기
- 모바일 목록 -> 상세 시트 -> 지도 복귀
- 비회원 좋아요/취소
- 공유 버튼 fallback
- 운영자 로그인 후 관리 진입과 로그아웃
- credentials 로그인
- credentials 회원가입
- 로그인 없는 장소 등록
- 로그인 없는 코멘트 작성/삭제
- 관리자 장소 승인
- 북마크 저장/해제
- 로그인 없는 신고 제출과 관리자 상태 변경
- 로그인 없는 가격 제보와 관리자 반려
- 관리자 승인 후 홈 검색 반영

Cloudflare 배포 전 점검은 아래 문서를 기준으로 합니다.

- [docs/deploy-cloudflare.md](/Users/alex/project/altteulmap/docs/deploy-cloudflare.md)
- [docs/cloudflare-account-to-deploy.md](/Users/alex/project/altteulmap/docs/cloudflare-account-to-deploy.md)

## CI/CD

- GitHub Actions: `Verify`, `E2E Smoke`, `E2E Full`, `Deploy Config Check`
- Cloudflare Builds: `main` push 후 자동 배포
- GitHub Actions: `Verify`, `E2E Full`, `Deploy Config Check`
- Cloudflare: public/admin split deploy + live `workers.dev` smoke

현재 권장 운영 방식은 `GitHub Actions가 검사`, `Cloudflare Builds가 배포`를 맡는 구조입니다.
## Stack

- `push to main`
- `Verify`: `npm run verify:quick`
- `E2E Smoke`: 로컬 Postgres service container + `npm run test:e2e:smoke`
- `Deploy Config Check`: 운영 env 기준 `npm run deploy:check`
- `pull_request`, `workflow_dispatch`
- `Verify`: `npm run verify`
- `E2E Full`: 로컬 Postgres service container + `npm run test:e2e`
- `Deploy Config Check`: GitHub repo `Secrets/Variables`에 저장한 운영 env로 `npm run deploy:check`
- Next.js 16, React 19, Tailwind CSS 4
- Cloudflare Workers, OpenNext, Wrangler
- PostgreSQL, Drizzle ORM
- NAVER Maps JavaScript API
- Auth.js credentials
- Playwright, ESLint

Playwright 브라우저는 GitHub Actions에서 `~/.cache/ms-playwright`를 캐시해 재실행 시간을 줄입니다.

GitHub Actions의 운영 env 이름은 `.env.production.local`과 동일하게 맞추는 것을 기준으로 합니다.

작업을 마치고 로컬 DB를 내리려면:
## Run Locally

```bash
npm run db:down
npm run db:up
npm run db:push
npm run db:seed
npm run dev
```

## Cloudflare 관련 파일

- `wrangler.jsonc`: Workers 설정 파일
- `wrangler.admin.jsonc`: 별도 관리자 Worker 설정 파일
- `open-next.config.ts`: OpenNext 설정 파일
- `.dev.vars`: 로컬 Cloudflare 개발용 변수
- `public/_headers`: 정적 자산 캐시 헤더
- App: `http://localhost:3000`
- Admin preview: `npm run preview:admin`
- README screenshots: `npm run readme:screenshots`

## 문서
## Next

- `prd.md`
- `trd.md`
- custom domain 최종 적용 여부 확정
- 모바일 제스처와 소규모 상호작용 polish
- 공유 telemetry를 추천/랭킹에 더 연결할지 결정
5 changes: 3 additions & 2 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
"name": "altteulmap-admin",
"private": true,
"scripts": {
"build": "rm -rf .next .next-dev && node ../../node_modules/next/dist/bin/next build --webpack",
"dev": "rm -rf .next-dev && node ../../node_modules/next/dist/bin/next dev --webpack -p 3001",
"build": "rm -rf .next && node ../../node_modules/next/dist/bin/next build --webpack",
"dev": "node ../../node_modules/next/dist/bin/next dev --webpack -p 3001",
"dev:clean": "rm -rf .next-dev",
"start": "node ../../node_modules/next/dist/bin/next start -p 3001"
}
}
1 change: 1 addition & 0 deletions apps/admin/src/app/api/telemetry/visit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { dynamic, POST } from "@/features/telemetry/api/visit-route";
21 changes: 18 additions & 3 deletions apps/admin/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Metadata } from "next";

import { GlobalHeader } from "@/components/global-header";
import { VisitTracker } from "@/features/telemetry/visit-tracker";
import { getSessionUser } from "@/lib/session";
import { createSiteUrl } from "@/lib/site";

import "./globals.css";

export const metadata: Metadata = {
Expand All @@ -10,14 +15,24 @@ export const metadata: Metadata = {
description: "알뜰맵 운영 콘솔",
};

export default function AdminRootLayout({
export default async function AdminRootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const user = await getSessionUser();

return (
<html lang="ko">
<body>{children}</body>
<html lang="ko" suppressHydrationWarning>
<body className="min-h-screen bg-stone-50 text-stone-900 antialiased">
<GlobalHeader
user={user}
adminHref="/admin"
homeHref={createSiteUrl("/").toString()}
/>
<VisitTracker scope="admin" />
{children}
</body>
</html>
);
}
Loading
Loading