규칙

개발

명령

pnpm install
pnpm infra         # docker 의 Postgres (dev · test 가 먼저 부른다). 끄기 pnpm infra:down
pnpm dev           # 기본 조합: 서버 셋(notes · tags · activity) + gateway + web → http://localhost:5280
pnpm test          # 모든 서버의 test (activity = Java 25, tags = uv 필요)
pnpm stack         # 서버 이미지를 빌드해 전체 스택을 띄우고 docker/smoke.sh. 끄기 pnpm stack:down
pnpm gen           # proto → contract/gen, 각 서버 코드 → contract/openapi/<서버>.json → .ts
pnpm typecheck     # gen + 모든 패키지의 typecheck
pnpm lint          # 모든 패키지의 lint (각자 도구)
pnpm format        # 모든 패키지의 format
pnpm --filter @app/<패키지> <스크립트>   # 하나만. 예: @app/activity dev, @app/admin dev, @app/mobile ios, @app/desktop start

필요한 도구: Node 24(.nvmrc) · pnpm 12 · Docker(compose v2.20+), activity 는 Java 25, tags 는 uv(Python 3.12), gateway 는 Go 1.25(go.mod), desktop 은 Rust(rust-toolchain.toml 이 버전을 고정, rustup 이 처음에 받는다).

포트

HTTP gRPC 확인용(verify-local, +100)
notes (Node) 8000 8100
activity (Spring) 8001 9001 8101 · 9101
tags (FastAPI) 8002 9002 8102 · 9102
gateway (Go) 8003 8103
web · admin · site · lite 5280 · 5281 · 5282 · 5283 5380 ~ 5383
desktop (Tauri 개발 서버) 1420
Postgres (pnpm infra) 5432 (같은 것을 쓰고 DB 만 <서버>_test)
  • 서버 포트는 PORT · GRPC_PORT, 바인딩은 HOST(기본 127.0.0.1, 컨테이너는 0.0.0.0), 앱이 부르는 주소는 API_URL(gateway, 기본 http://127.0.0.1:8003. vite 는 dev 때, next 는 빌드 때)로 바꾼다. gateway 가 넘길 서버는 NOTES_URL · TAGS_URL · ACTIVITY_URL, notes 가 부르는 서버는 TAGS_GRPC · ACTIVITY_GRPC(기본 http://127.0.0.1:9002 · :9001).
  • 에이전트는 사용자가 띄운 개발 서버를 끄거나 거기에 요청하지 않는다. 확인은 +100 포트 · 테스트 DB(<서버>_test)로 따로 띄우고, 끌 때는 그 포트의 PID · 자기가 띄운 컨테이너만 끈다 (pkill -f 금지: 같은 명령줄의 다른 프로젝트 서버까지 꺼진다). Postgres 는 사용자의 개발 DB 도 들어 있으니 down -v 로 지우지 않는다(사용자가 말할 때만).

환경 변수

.env 는 서버마다 선택 (servers/<서버>/.env.template 참고). HOST · PORT · GRPC_PORT, DB 는 libpq 와 같은 PGHOST · PGPORT · PGDATABASE · PGUSER · PGPASSWORD (기본: pnpm infra 의 127.0.0.1:5432, DB = 서버 이름, app/app). 테스트는 PGDATABASE 만 <서버>_test 로 바꾼다.

인프라 (docker/)

docker/폴더 구조
  • 개발 · 테스트 · CI 가 함께 사용하는 Postgres 를 실행합니다.
  • 서버 · 웹 앱 이미지와 공통 인프라를 함께 실행합니다.
  • 서버마다 개발 DB 와 테스트 DB 를 만듭니다. 볼륨을 처음 만들 때 실행됩니다.
  • 전체 스택의 API · 웹 화면 · 프록시 연결을 확인합니다.
  • docker/.env 로 복사해 DB 와 서버의 포트를 설정합니다.
  • 두 파일의 compose 프로젝트 이름(name:)이 같아서 같은 Postgres 를 쓴다. 데이터는 볼륨 <이름>_db 에 남는다. 처음부터 다시 만들기 · 다른 DB 로 바꾸기는 guides/database.md.
  • 전체 스택은 서버 · gateway 의 HTTP 와 웹 앱만 이 맥(127.0.0.1)에 연다. 서버끼리는 compose 네트워크 안에서 gRPC 로 부른다(notes → tags:9002 · activity:9001). 개발 서버와 같이 띄우려면 NODE_PORT · WEB_PORT 등을 다르게 (docker/.env.example).
  • 5432 가 이미 쓰이면 docker/.env 에 POSTGRES_PORT, 서버 · 테스트에는 같은 값으로 PGPORT.

이미지

  • 서버 · 웹 앱마다 Dockerfile (mobile · desktop 은 네이티브 빌드라 없다). 컨텍스트는 그 폴더이고, 저장소 루트의 공유 파일 (lockfile · contract/ · @app/client · tsconfig.base.json)은 이름 붙인 컨텍스트 workspace 에서 COPY --from=workspace 로 받는다. BuildKit 은 COPY 한 경로만 보내서 저장소 전체가 넘어가지 않는다. gateway 는 공유 파일이 필요 없어 그 폴더만 쓴다.
  • 하나만: 그 폴더에서 docker build --build-context workspace=../.. . (앱은 ../../..). compose 는 additional_contexts.
  • 단계를 나눠 실행 이미지에는 실행에 쓰는 것만(dev 의존성 · 빌드 도구 · 캐시 없음), 실행 사용자는 root 가 아니다.
  • 폴더를 통째로 COPY 하는 이미지는 그 폴더에 .dockerignore (activity: build · .gradle, tags: __pycache__). 나머지는 경로를 골라 COPY 한다 (node_modules 를 넘기면 이미지 안에서 설치한 것을 덮는다).
  • web · admin · lite: dist + nginx(clients/nginx.conf.template), /api → API_URL(실행할 때). site: standalone, rewrites 의 API_URL 은 빌드 인자 · 서버 컴포넌트는 실행할 때. 기본은 compose 의 gateway(http://gateway:8003).
  • gateway 이미지의 NOTES_URL · TAGS_URL · ACTIVITY_URL 은 compose 가 서비스 이름(http://notes:8000 …)으로 준다.

CI (.github/)

workflows/ci.yml 의 job 하나(CI)가 PR · main push · merge queue 마다 루트 명령을 차례로 돌린다. 명령이 로컬과 같아서 실패하면 같은 명령을 로컬에서 돌려 본다.

pnpm install → proto 호환 → 템플릿 스크립트 목록(rename · drop --check) → pnpm infra → pnpm typecheck(gen 포함) → 생성물이 커밋과 같은가 → pnpm lint → pnpm -r test
  • 브랜치 보호의 필수 검사는 CI 하나다. 서버 · 앱을 더하거나 빼도 워크플로를 고치지 않는다.
  • proto 호환(buf breaking)은 PR 의 대상 브랜치 커밋, merge queue 의 기준 커밋, main push 의 직전 커밋과 비교한다. 첫 push 는 이전 커밋이 없어 이 검사만 건너뛴다.
  • 전체 스택(이미지 · smoke)은 CI 에서 돌리지 않는다. 이미지 · 네트워크를 바꿨으면 로컬에서 pnpm stack.
  • 생성물이 다르다고 실패하면 로컬에서 pnpm gen 을 돌려 함께 커밋한다.
  • PR 은 .github/pull_request_template.md 의 확인 목록을 채운다. 한 PR 에는 한 작업만.
  • 같은 PR 의 이전 실행은 취소한다(concurrency). 권한은 contents: read 만.
  • 워크플로를 바꾸면 actionlint 로 확인한다 (docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest, git 저장소 안에서).

의존성 설치

  • 설치 스크립트는 기본으로 막혀 있다(pnpm 12, 막힌 것이 있으면 pnpm install 이 실패한다). 스크립트 없이 동작하는지 확인한 것만 pnpm-workspace.yaml 의 allowBuilds 에서 false 로 끄고 이유를 주석에 쓴다. 꼭 돌려야 하는 것만 true.
  • pnpm 은 나온 지 하루가 안 된 버전을 설치하지 않는다(minimumReleaseAge). 막히면 예외(minimumReleaseAgeExclude)를 두지 않고 범위의 아래 끝을 그 전 버전으로 낮춘 뒤(예: ~58.0.14 → ~58.0.12), 하루가 지나면 pnpm update. lockfile 에 이미 막힌 버전이 있으면 pnpm 이 lockfile 을 거부하니 pnpm-lock.yaml 을 지우고 pnpm install 로 다시 만든다 (범위 안에서만 바뀐다).
  • pnpm-workspace.yaml 을 통째로 다시 쓰면 pnpm 이 넣은 항목이 사라진다. 고칠 때는 덧붙인다.

이름 바꾸기

템플릿으로 저장소를 만든 뒤 한 번 pnpm rename "<표시 이름>" [slug]. 템플릿 이름 iStack(README · AGENTS · DESIGN 제목, 앱 title · Tauri 창 · Expo 앱 이름)과 slug initializer-stack(루트 package.json · compose 프로젝트, Expo slug <slug>-mobile, Tauri identifier <slug>.desktop)을 바꾸고 스크립트는 스스로 지워진다. slug 를 생략하면 표시 이름에서 만든다(My Shop → my-shop, 영문이 아니면 slug 를 준다).

  • 저장소 안에서만 쓰는 이름은 그대로 둔다: @app/* scope, Java 패키지 app, Postgres 계정 app. 밖에 보이지 않아 프로젝트 이름과 달라도 된다.
  • proto 패키지 app.tag.v1 · app.activity.v1 은 gRPC 경로(/app.tag.v1.TagService/Suggest)로 밖에 보인다. 다른 저장소와 proto 를 나눠 쓰게 되면 첫 배포 전에 바꾼다: contract/proto/<이름>/ 로 옮기고 package · 각 서버의 import · gRPC 테스트 이름을 고친 뒤 pnpm gen.
  • 템플릿에 보이는 이름을 더하면 scripts/rename.mjs 의 목록에도 더한다. CI 가 --check 로 목록이 파일과 맞는지 본다. 템플릿 이름 자체를 바꿀 때는 목록의 곳을 모두 고친 뒤 스크립트의 FROM 한 줄.

예시 빼기

쓰지 않는 예시 서버 · 앱은 pnpm drop <이름...> 으로 뺀다(예: pnpm drop site lite mobile). 폴더와 함께 그것을 가리키는 설정 줄을 지운다: 루트 gen · dev, contract/index.ts · contract/openapi/<서버>.*, docker/postgres/init.sql, compose 서비스, smoke.sh 포트, docker/.env.example. 끝나면 pnpm install 로 lockfile 을 정리한다.

  • 남는 것이 부르는 서버는 빼지 않는다: 앱은 createClient<서버.paths>, 서버는 <서버>_GRPC · <서버>_URL 로 찾는다. tags · activity 는 notes · admin · gateway 가 부르니 함께 빼거나 그 호출을 고친 뒤 다시 돌린다. gateway 는 앱이 하나라도 남으면 빼지 않는다(앱은 모두 gateway 로 부른다).
  • 문서(README · docs · AGENTS.md)의 설명은 고치지 않고, 남은 언급이 있는 파일을 알려 준다. ADR 은 기록이라 그대로 둔다.
  • 템플릿에 서버 · 앱을 더하면 scripts/drop.mjs 의 목록에도 더한다. CI 가 --check 로 지울 줄이 파일에 있는지 본다.
GitHub 에서 고치기