title: "Project Structure"¶
status: Approved owner: pablo audience: developer last_updated: 2026-04-13 reviewers: [] tags: [guide, structure, monorepo]
Project Structure¶
레포 최상위부터 각 디렉토리의 역할을 설명. 파일을 추가할 때 "어디에 둘지" 판단 기준과 "왜 그 구조인지" 의 rationale 을 함께 제공한다.
Top-level layout¶
Umbra/
├─ apps/ # 프로세스 진입점 (bot, api, worker, web)
├─ engine/ # 도메인 코어 (Go 프로세스 공유)
├─ platform/ # 인프라 유틸 (Go 프로세스 공유)
├─ db/ # SQL schema, migrations, sqlc queries
├─ deployments/ # fly.toml, vercel config
├─ docs/ # documentation (single source of truth)
├─ .github/ # GitHub workflows, PR/issue 템플릿
├─ .claude/ # Claude Code 설정 (luxtra-agent 플러그인)
├─ go.mod, go.sum # 단일 Go module
├─ package.json # Bun workspace root
├─ bun.lockb # Bun lockfile
├─ mise.toml # Tool 버전 고정
├─ atlas.hcl # Atlas 설정
├─ sqlc.yaml # sqlc 설정
├─ docker-compose.yml # 로컬 infra
├─ justfile # 태스크 오케스트레이터 (make 대체)
├─ README.md
├─ LICENSE
├─ .editorconfig
├─ .env.example # 환경변수 템플릿
├─ .bunfig.toml # Bun 설정 (registry mirror 등)
└─ .gitignore
apps/ — 프로세스 진입점¶
각 프로세스가 독립 배포 단위 (ADR-0017).
apps/
├─ bot/ # Discord Gateway
│ ├─ cmd/main.go
│ ├─ internal/ # Go keyword: 이 프로세스 전용 코드
│ │ ├─ handler/ # Gateway event handlers
│ │ └─ config/
│ └─ fly.toml
│
├─ api/ # HTTP server (dashboard, OAuth2, webhooks)
│ ├─ cmd/main.go
│ ├─ internal/
│ │ ├─ handler/
│ │ ├─ middleware/
│ │ ├─ dto/
│ │ └─ config/
│ └─ fly.toml
│
├─ worker/ # asynq + Temporal + Outbox poller
│ ├─ cmd/main.go
│ ├─ internal/
│ │ ├─ cron/ # asynq cron 등록
│ │ ├─ jobs/ # asynq task handlers
│ │ ├─ workflow/ # Temporal workflow/activity
│ │ ├─ outbox/ # Outbox poller
│ │ └─ config/
│ └─ fly.toml
│
└─ web/ # React SPA (Vercel)
├─ src/
│ ├─ features/ # 기능별 (billing, restore, ...)
│ ├─ components/ # shadcn/ui 전용
│ ├─ shared/ # 공통 컴포넌트, 유틸
│ ├─ routes/ # TanStack Router
│ ├─ types/ # generated (api.gen.ts)
│ ├─ styles/
│ └─ main.tsx
├─ public/
├─ index.html
├─ vite.config.ts
├─ tsconfig.json
└─ package.json
Why internal/¶
Go 의 internal/ 는 특별 키워드로 같은 module tree 바깥에서 import 불가. 즉 apps/bot/internal/handler 는 apps/api 가 import 못 한다. 프로세스 경계 강제에 기여 (ADR-0018).
Why apps/web/src/features/¶
단순 components/ vs features/ 분리 — shadcn 컴포넌트와 비즈니스 컴포넌트를 섞지 않는다. 자세한 건 frontend-conventions.md 참조.
engine/ — 도메인 코어¶
세 Go 프로세스(bot / api / worker)가 공유하는 도메인 로직.
engine/
├─ identity/ # Supporting (단순 구조)
│ ├─ entity.go
│ ├─ service.go
│ ├─ repository.go
│ ├─ events.go
│ └─ adapter/
│ ├─ discord/
│ └─ persistence/
│
├─ guild/ # Supporting
├─ member/ # Supporting
├─ notification/ # Supporting
│
├─ licensing/ # Core (full Hexagonal)
│ ├─ domain/ # 순수 entity, invariant
│ ├─ port/ # 인터페이스 (repository, toss, event, ...)
│ ├─ app/ # 유스케이스 구현
│ └─ adapter/ # port 구현
│ ├─ persistence/sqlc/
│ ├─ cache/
│ └─ event/
│
├─ billing/ # Core
├─ recovery/ # Core (4 sub-contexts)
│ ├─ shared/ # 공유 port, adapter, types
│ └─ subdomain/
│ ├─ sync/
│ ├─ snapshot/
│ ├─ restore/
│ └─ antinuke/
│
├─ audit/ # Generic (단순 구조)
└─ webhook/ # Generic
Why Core vs Supporting¶
ADR-0019 참조. Core 는 복잡도와 중요도가 높아 Hexagonal 풀세트, Supporting/Generic 은 단순 2층 구조.
File naming¶
- Core domain:
domain/에 entity 당 파일 분리 (subscription.go,billing_key.go) - Supporting: 도메인 당
entity.go,service.go,repository.go,events.go네 파일 권장 - Test:
*_test.go같은 파일 옆에
platform/ — 인프라 유틸¶
도메인 로직이 아닌 공통 도구. Core/Supporting/Generic 모두가 import.
platform/
├─ event/ # Outbox 구현
├─ db/ # sqlc 초기화, 트랜잭션 래퍼
├─ redis/ # Redis client pool
├─ temporal/ # Temporal client, registration helper
├─ uuid/ # uuid.NewV7 래퍼
├─ crypto/ # AES-256-GCM helpers
├─ ctx/ # context keys (actor, request_id)
├─ telemetry/ # OpenTelemetry 설정
├─ config/ # envconfig wrapper
├─ logger/ # slog 설정
└─ clock/ # time.Now + jitter (테스트용 mock)
Why 별도 platform/¶
engine/ 은 도메인 의도, platform/ 은 인프라 수단. 같은 디렉토리에 넣으면 경계가 흐려지고 도메인 코드가 인프라 세부를 알게 된다.
db/ — SQL source of truth¶
db/
├─ schema/ # Declarative DDL (Atlas source)
│ ├─ 00_schemas.sql # CREATE SCHEMA ...
│ ├─ identity.sql
│ ├─ guild.sql
│ ├─ member.sql
│ ├─ licensing.sql
│ ├─ billing.sql
│ ├─ recovery.sql
│ ├─ notification.sql
│ ├─ audit.sql
│ ├─ webhook.sql
│ └─ events.sql
│
├─ migrations/ # Atlas 자동 생성
│ ├─ 20260401_init.sql
│ └─ atlas.sum
│
├─ queries/ # sqlc source
│ ├─ identity/
│ │ ├─ users.sql
│ │ └─ sessions.sql
│ ├─ guild/
│ └─ ... # 도메인별
│
└─ seed/ # 초기 seed 데이터 (just db-seed)
└─ plans.sql
Why schema-per-domain¶
ADR-0020. 하나의 PostgreSQL schema 당 하나의 도메인. sqlc 패키지도 도메인별 분리 → engine/{domain}/adapter/persistence/sqlc/ 와 1:1.
SQL 작성 규칙¶
db/schema/*.sql이 desired state (Atlas)- 수정 시 선언 스키마 변경 →
atlas migrate diff→ 생성된 migration 커밋 db/queries/*.sql은 sqlc 에 의해 Go 로 코드 생성- 자세한 컨벤션은
database-conventions.md
deployments/¶
각 apps/*/fly.toml 은 해당 apps 디렉토리에 위치. 공통 base Dockerfile 만 여기.
docs/¶
docs/
├─ README.md
├─ LICENSE (doc license, source code LICENSE 와 별개)
├─ .templates/
├─ overview/
├─ architecture/
├─ adr/
├─ roadmap/
├─ domain/
├─ data/
├─ flows/
└─ guides/
documentation-conventions.md 참조.
.github/¶
.github/
├─ workflows/ # GitHub Actions
│ ├─ ci.yml
│ ├─ deploy-bot.yml
│ ├─ deploy-api.yml
│ ├─ deploy-worker.yml
│ └─ deploy-web.yml (Vercel auto)
├─ PULL_REQUEST_TEMPLATE.md
└─ ISSUE_TEMPLATE/
├─ bug.yml
├─ feature.yml
├─ research.yml
├─ chore.yml
└─ docs.yml
템플릿은 luxtra-agent 플러그인의 것을 준용.
.claude/¶
Claude Code (luxtra-agent) 설정. 팀 공용.
.claude/
├─ CLAUDE.md # 프로젝트 컨텍스트
├─ settings.json # 허용된 plugin 목록
├─ memory/ # TODO, SESSION, DECISIONS, HISTORY (git-ignored)
│ ├─ TODO.md
│ ├─ SESSION.md
│ ├─ DECISIONS.md
│ └─ HISTORY.md
└─ reports/ # codebase-analyzer, research 출력 (git-ignored)
Root-level files¶
go.mod¶
단일 module — module github.com/luxtradev/Umbra. Apps 각각 별도 module 아님. 이유: 도메인 코드 공유 (ADR-0018) 가 자연스러움. go.work 는 도입하지 않음 (ADR-0009).
package.json (루트)¶
Bun workspace root. 오케스트레이션은 justfile 이 담당하므로 scripts 는 비워둔다:
개별 앱의 scripts (예: apps/web/package.json 의 Vite dev/build/test) 는 그대로 유지한다. 이들은 workspace 내부 관례이지 cross-process 오케스트레이션이 아니다.
justfile¶
모든 개발·DB·코드 생성·린트·테스트 태스크의 단일 오케스트레이터. just 를 설치하면 just 만 쳐도 recipe 목록이 출력된다.
# Umbra task orchestrator — run `just` to list all recipes
default:
@just --list
# ── Development ───────────────────────────────────────
dev-bot:
cd apps/bot && air
dev-api:
cd apps/api && air
dev-worker:
cd apps/worker && air
dev-web:
bun --cwd apps/web run dev
# ── Database ──────────────────────────────────────────
db-migrate:
atlas migrate apply --env local
db-seed:
psql $DATABASE_URL -f db/seed/plans.sql
db-reset: db-migrate db-seed
# ── Code generation ───────────────────────────────────
sqlc:
sqlc generate
sqlc-clean:
rm -rf engine/*/adapter/persistence/sqlc
openapi:
swag init -g apps/api/cmd/main.go -o docs/api
bunx openapi-typescript docs/api/openapi.json -o apps/web/src/types/api.gen.ts
# ── Lint ──────────────────────────────────────────────
lint-go:
golangci-lint run ./...
lint-web:
bun --cwd apps/web run lint
lint: lint-go lint-web
# ── Test ──────────────────────────────────────────────
test-go:
go test ./...
test-web:
bun --cwd apps/web run test
test-integration:
go test -tags=integration ./...
test: test-go test-web
make 는 사용하지 않는다. 이유는 architecture/tech-stack.md 참조.
mise.toml¶
새 개발자의 tool 버전 자동 맞춤.
Where to put new code¶
새 도메인 추가¶
- Supporting/Generic 이면
engine/{name}/단순 구조 - Core 면
engine/{name}/{domain,port,app,adapter}/풀세트 - 새 schema 를
db/schema/{name}.sql에 추가 - sqlc queries 를
db/queries/{name}/에 - 도메인 문서
docs/domain/{name}.md, 스키마 문서docs/data/{name}-schema.md
새 API endpoint¶
apps/api/internal/handler/{feature}.go- DTO 는
apps/api/internal/dto/{feature}.go - swag annotation 필수 (ADR-0030)
- OpenAPI 재생성 후
apps/web/src/types/api.gen.tscommit
새 Discord slash command¶
apps/bot/internal/handler/commands/{command}.go- 도메인 호출은
engine/{domain}.Service를 직접
새 asynq job¶
- Handler:
apps/worker/internal/jobs/{job}.go - Enqueue: 도메인 서비스 내부에서
client.Enqueue(...)호출 - Cron:
apps/worker/internal/cron/{feature}.go
새 Temporal workflow¶
engine/recovery/subdomain/{sub}/adapter/temporal/workflow.go- Activity:
activities.go - Worker 등록:
apps/worker/internal/workflow/register.go
새 React 기능¶
apps/web/src/features/{feature}/(route + components + hooks)- 공용 UI 는
apps/web/src/components/ui/(shadcn 만) - shared utility 는
apps/web/src/shared/
새 ADR¶
docs/adr/XXXX-short-title.md(번호는 가장 큰 것 + 1)docs/adr/.templates/adr.template.md사용docs/adr/README.md에 목록 업데이트
Cross-reference¶
- 새 결정 추가 시 관련 도메인 / flow 문서의 "See also" 에 링크 추가
- Breaking change 시 ADR 로 기록
See also¶
getting-started.mddocumentation-conventions.mdbackend-conventions.mdfrontend-conventions.mdhexagonal-pattern.md../architecture/overview.md../adr/0009-monorepo-bun-workspace.md../adr/0017-process-separation-bot-api-worker.md../adr/0018-domain-code-sharing.md../adr/0019-hexagonal-pragmatic.md