# ARA SYSTEM 사내 그룹웨어다. 첫 모듈은 **메일** — 웹메일(포털)과 휴대폰 앱(안드로이드)이다. 메일 엔진(Stalwart)이 메일을 주고받고, 우리 서버(NestJS)가 엔진을 감싸 웹과 앱에 같은 API 를 준다. 화면은 직접 만든다(Vue 3 · Flutter). `@arasystem.it.kr` 메일은 2026-11-05 옛 서버(mailcow)에서 옮겨 와 전 직원이 쓰고 있다. ## 지금 - 운영: 메일 1~13단계. 메일 14단계(자체 검색 색인 · 같은 대화)와 앱 첫 판 · 휴대폰 앱 받기는 main 에 합쳤고 서버 배포를 기다린다. - 개발 중: 메일 15단계(메일 · 메일함 다루기). 다음은 16단계(개인 설정 — 주소록 가져오기 · 여러 서명 등). - 단계별 내용 · 다음 차례 · 미뤄 둔 후보는 [`docs/roadmap.md`](./docs/roadmap.md). ## 구성 | 이름 | 스택 | 하는 일 | 포트(개발) | | --- | --- | --- | --- | | `backend/` | NestJS · TypeORM | 웹 · 앱이 함께 쓰는 REST API — 메일 엔진 중계, 로그인 세션, 검색 색인, 앱 알림, 대용량 첨부 | 3000 | | `frontend/` | Vue 3 · Vite | 웹메일 화면. 운영은 nginx 가 화면과 같은 주소의 `/api` 를 백엔드로 넘긴다 | 5173 | | `flutter/` | Flutter · Riverpod | 휴대폰 앱(안드로이드 — 스토어 없이 사내 배포) | - | | `db` | PostgreSQL 15 | 로그인 세션 · 설정 · 검색 색인 · 대용량 첨부 기록 등 | 5432 | | `mail` | Stalwart | 메일 엔진(JMAP · IMAP · SMTP) — 직원 로그인 = 메일 계정 | 관리 8180(운영은 25 · 465 · 993 · 443) | | `mail-resolver` | unbound | 메일 엔진 전용 DNS(스팸 차단 목록 · DNSSEC) | - | | `clamav` | ClamAV | 대용량 첨부 악성 검사(운영은 늘, 개발은 profile `clamav`) | - | | `mailpit` | Mailpit | 개발 전용 — 바깥 주소로 보낸 메일을 모은다 | 8025 | | `redis` | Redis | 기본 꺼짐 — 서버를 여러 대로 늘릴 때의 자리 | 6379 | | `ops/` | 셸 · systemd | 운영 서버의 백업(restic) · 15분 점검 · 부팅 때 되살리기 | - | | `ci/` | Gitea Actions 러너 | PR 마다 lint · 시험 · 빌드 · 인프라 검사 | - | ## 문서 지도 | 알고 싶은 것 | 문서 | | --- | --- | | 서버 · 웹 · 앱이 함께 지키는 API 약속 | [`docs/mail/api-contract.md`](./docs/mail/api-contract.md) | | 웹 화면 디자인 · 화면 문구 규칙 · 단계별 화면 | [`docs/design/webmail/README.md`](./docs/design/webmail/README.md) — 디자인 원본은 같은 폴더의 `*.dc.html`(캔버스 보드) | | 앱 화면 | [`docs/design/app/README.md`](./docs/design/app/README.md) | | 메일 엔진 설정 · 운영 배포 · 백업 · 감시 · 전환 | [`mail/README.md`](./mail/README.md) | | 앱 개발 실행 · 사내 배포(서명 키 · APK 올리기) | [`flutter/README.md`](./flutter/README.md) | | CI 러너 | [`ci/README.md`](./ci/README.md) | | 개발 규칙(공통 · 스택별 · 역할) | [`CLAUDE.md`](./CLAUDE.md) · 아래 "개발 규칙" | | 로드맵 | [`docs/roadmap.md`](./docs/roadmap.md) | | 로고 · 색 | [`docs/brand/`](./docs/brand/) | ## 사전 요구사항 - Docker / Docker Compose — 백엔드 · DB · 메일 엔진(컨테이너 안 Node 는 24 LTS 로 고정 — docker 스킬 "Node 버전") - Node.js 24 LTS — 호스트에서 웹 개발 서버 · 메일 setup 스크립트를 돌릴 때 - Flutter 3.41.9(Dart 3.11.5) — 앱(compose 대상이 아니다 — [`flutter/README.md`](./flutter/README.md)) ## 빠른 시작 (개발) ```bash # 1) 환경변수 파일 — 값은 루트 .env.development 한 곳에 쓴다(SSOT) cp .env.development.example .env.development cp backend/.env.development.example backend/.env.development # 있어야 한다(JWT 등 백엔드 고유 비밀) # 2) 백엔드 스택 — backend + db, 기본 COMPOSE_PROFILES=mail 이면 mail · mail-resolver · mailpit 까지 docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d # 3) 메일 엔진 초기화 — 처음 한 번(여러 번 돌려도 안전하다). 개발 시험 계정도 여기서 만든다(mail/README.md) node --env-file=.env.development mail/scripts/setup.mjs # 4) 웹은 호스트에서 띄운다 — /api 는 vite 프록시가 백엔드 컨테이너로 넘긴다 cd frontend && npm install && npm run dev ``` - 웹 · API · Swagger - 의존성을 바꿨을 때만 `up -d --build`(백엔드는 `--renew-anon-volumes` 도 — docker 스킬). - 백엔드를 처음 띄우면 watch 빌드에 수십 분이 걸릴 수 있다(Windows 바인드 마운트). 브랜치를 바꿨다고 다시 시작하지 않는다. - 웹을 컨테이너로 띄워야 하면 `--profile frontend`(느리다 — 호스트 vite 와 함께 띄우지 않는다). 운영(`docker-compose.yml` 단독)은 nginx 정적 호스팅이다. - 포트는 루트 `.env.{env}` 를 따른다(`FRONTEND_PORT` · `BACKEND_PORT` — `vite.config.ts` 가 직접 읽는다). 포트가 이미 쓰이면 다른 포트로 옮기지 않고 실패한다. - Redis 는 뜨지 않는다. 필요해지면 `COMPOSE_PROFILES` 에 `redis` 를 더한다. 전체 실행 · 배포 명령은 [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md). ## 앱 실행 (Flutter) ```bash cd flutter && cp .env.example .env && flutter pub get && flutter run ``` `flutter/.env` 의 `API_URL` 을 백엔드 주소로 둔다(Android 에뮬레이터는 `http://10.0.2.2:3000/api`). 실기기 · 새 메일 알림 · 사내 배포는 [`flutter/README.md`](./flutter/README.md). ## 환경변수 `.env*` 는 커밋하지 않는다. 각 자리의 `*.example` 을 복사해 쓴다. **루트 `.env.{env}` 가 단일 진실 공급원(SSOT)**이다 — backend · frontend 는 compose 가 루트 값을 넣어 준다 (구조는 [`.claude/rules/env-structure.md`](./.claude/rules/env-structure.md)). | 파일 | 용도 | 값을 쓰는 곳 | | --- | --- | --- | | `.env.{env}.example` | 루트 — 모든 설정의 SSOT(compose 가 넣어 준다) | 여기 | | `backend/.env.{env}.example` | 백엔드 고유 비밀(JWT · 앱 알림 키 등)만 | 최소(파일은 있어야 한다) | | `flutter/.env.example` | 앱 — `API_URL`(compose 대상이 아니다) | 따로 | `{env}` = `development` | `production`. 운영 서버의 백업 · 점검 설정은 `/etc/arasystem/`(`ops/ops.env.example` — mail/README "백업"). ## 표준 응답 포맷 ```jsonc // 성공 { "success": true, "data": { /* ... */ } } // 실패 — 사용자에게 보이는 모든 실패는 에러 코드를 가진다 { "success": false, "error": { "code": "VAL_001", "message": "...", "traceId": "3f1c0a2e-..." } } // 목록 — data 안의 꼴이 정해져 있다 { "success": true, "data": { "items": [], "pageInfo": { "hasNext": false, "nextCursor": null, "total": 0 } } } ``` `traceId` 는 요청마다 붙는 추적 ID(`x-request-id` 헤더와 같다)라 서버 로그에 같은 값이 남는다 — 문의가 오면 이 값으로 로그를 찾는다. `/api/health` 는 예외다(모니터링 도구가 읽는 원본 꼴). ### 에러 코드는 한 곳에서 ``` backend/src/common/errors/error-codes.ts ← 여기만 고친다 │ cd backend && npm run sync:error-codes ├─▶ frontend/src/shared/constants/error-codes.generated.ts (자동 생성) └─▶ flutter/lib/core/api/error_lexicon.g.dart (자동 생성) ``` 생성 파일은 직접 고치지 않는다. CI 가 맞는지 본다 — [`.claude/rules/error-codes.md`](./.claude/rules/error-codes.md). ## 커밋 전 검사 ```bash # backend — 컨테이너 안에서(의존성 · lock 이 Linux 기준이라 호스트에서 npm install 하지 않는다) docker exec arasystem-backend npm run lint docker exec arasystem-backend npm run test # 메모리가 빠듯하면 -- --maxWorkers=2 docker exec arasystem-backend npx jest --config ./test/jest-e2e.json # e2e — DB · 메일 엔진이 떠 있어야 한다 # frontend · flutter — 호스트에서 cd frontend && npm run lint && npm run type-check && npm run test:unit # 시간대가 걸린 화면을 고쳤으면 TZ=UTC 로도 cd flutter && flutter analyze && flutter test ``` 컨테이너 이름은 `-backend`(개발 기본 `arasystem`). lint 는 `--max-warnings 0` 이라 경고 하나도 실패다. 같은 검사를 CI(`.github/workflows/ci.yml` · `ci-flutter.yml`)가 PR 마다 한다. > ⚠️ **백엔드 의존성은 컨테이너 안에서만 바꾼다** — Windows 에서 `npm install` 하면 Linux 전용 패키지가 lock 에서 빠져 Docker 빌드가 깨진다. > `docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development exec backend npm install <패키지>` ## 개발 규칙 | 영역 | 문서 | | --- | --- | | 입구 · 공통 | [`CLAUDE.md`](./CLAUDE.md) · [`.claude/rules/`](./.claude/rules/) | | 응답 · 에러 | [`response-format.md`](./.claude/rules/response-format.md) · [`error-codes.md`](./.claude/rules/error-codes.md) | | API 설계 | [`api-design.md`](./.claude/rules/api-design.md) | | 로깅 · 보안 | [`logging.md`](./.claude/rules/logging.md) · [`security.md`](./.claude/rules/security.md) | | 점검 목록 | [`checklist.md`](./.claude/rules/checklist.md) | | NestJS | [`.claude/skills/nestjs/SKILL.md`](./.claude/skills/nestjs/SKILL.md) | | Vue 3 | [`.claude/skills/vue3/SKILL.md`](./.claude/skills/vue3/SKILL.md) | | Flutter | [`.claude/skills/flutter/SKILL.md`](./.claude/skills/flutter/SKILL.md) | | Docker · 운영 | [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md) | | 역할 분담 | [`.claude/agents/README.md`](./.claude/agents/README.md) | 새 화면은 캔버스 보드로 먼저 확인받은 뒤 만든다(디자인 README). 화면 · 알림 글은 디자인 README 「화면 문구 — 사용자에게 말하듯」을 따른다. ## 디렉터리 구조 ``` . ├── backend/ # NestJS API — modules/(auth · mail · contacts · file-storage · push · ops 등) · common · shared ├── frontend/ # Vue 3 웹메일 — pages · components · composables · stores · router, 운영 nginx 설정 ├── flutter/ # 휴대폰 앱 — core · features · shared ├── mail/ # 메일 엔진 setup 스크립트 · DNS 리졸버 · ClamAV 설정 · 운영 문서(README) ├── ops/ # 운영 서버의 백업 · 점검 · 부팅 되살리기(systemd) ├── docs/ # API 계약 · 화면 디자인(캔버스 보드) · 브랜드 · 로드맵 ├── ci/ # CI 러너(Gitea Actions) — 앱 스택과 따로 띄운다 ├── data/ # 개발용 바인드 마운트 자리(내용은 커밋하지 않는다) ├── docker-compose.yml # 운영 · 개발 공통 정의 ├── docker-compose.dev.yml # 개발 오버라이드(핫리로드 · 127.0.0.1 포트) ├── docker-compose.remote-proxy.yml # 리버스 프록시가 다른 서버에 있을 때(운영 선택) ├── CLAUDE.md # 개발 규칙의 입구(.claude/ 의 규칙을 불러온다) ├── .github/ # CI 워크플로(ci.yml · ci-flutter.yml) · PR 양식 └── .claude/ # rules(공통 규칙) · skills(스택별 규칙) · agents(역할) · 설정 ```