콘텐츠로 이동

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/handlerapps/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/

deployments/
├─ fly/           # Fly.io 관련 (fly.toml 템플릿 등)
│  └─ Dockerfile.base
└─ vercel/
   └─ vercel.json

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 는 비워둔다:

{
  "name": "umbra",
  "private": true,
  "workspaces": ["apps/web"]
}

개별 앱의 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

[tools]
go    = "1.23.x"
bun   = "1.1.x"
just  = "1.x"
atlas = "0.x"
sqlc  = "1.x"

새 개발자의 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.ts commit

새 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.md
  • documentation-conventions.md
  • backend-conventions.md
  • frontend-conventions.md
  • hexagonal-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