- 폭 360 에서 검색 칸이 상단 바 밖으로 넘쳐 화면이 옆으로 밀렸다(14단계부터) — 메일 검색 칸 · 비활성 통합 검색 칸의 최소 폭을 0 으로 둔다
- 읽기 칸 도구 줄이 이름을 숨기는 폭을 570 · 595 에서 580 · 605 로 올린다 — 실측한 필요 폭(569.19 · 594.83)에 여유가 1px 도 없어 글자 · 여백이 조금만 늘어도 두 줄로 꺾였다
- 잘못 놓은 파일 무시를 메일 화면(MailLayout)에서 앱 틀(AppShell)로 옮긴다 — 주소록 · 설정에 놓은 파일은 브라우저가 열어 웹메일을 떠났다. 쓰기 화면 · 서명 설정은 지금처럼 받는다
- 받은편지함 정리를 켠 차단 뒤에는 다음 메일 대신 목록으로 간다 — 다음 메일이 같은 보낸 사람의 것이면 열자마자 읽음이 되고 곧 정리가 스팸메일함으로 옮겼다
- 모달 창의 제목 · 글은 낱말 가운데서 줄을 바꾸지 않고("지울까/요?") 띄어쓰기 없는 긴 글만 아무 데서나 끊는다
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ARA SYSTEM
사내 그룹웨어다. 첫 모듈은 메일 — 웹메일(포털)과 휴대폰 앱(안드로이드)이다.
메일 엔진(Stalwart)이 메일을 주고받고, 우리 서버(NestJS)가 엔진을 감싸 웹과 앱에 같은 API 를 준다. 화면은 직접 만든다(Vue 3 · Flutter).
@arasystem.it.kr 메일은 2026-11-05 옛 서버(mailcow)에서 옮겨 와 전 직원이 쓰고 있다.
지금
- 운영: 메일 1~13단계. 메일 14단계(자체 검색 색인 · 같은 대화)와 앱 첫 판 · 휴대폰 앱 받기는 main 에 합쳤고 서버 배포를 기다린다.
- 개발 중: 메일 15단계(메일 · 메일함 다루기). 다음은 16단계(개인 설정 — 주소록 가져오기 · 여러 서명 등).
- 단계별 내용 · 다음 차례 · 미뤄 둔 후보는
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/design/webmail/README.md — 디자인 원본은 같은 폴더의 *.dc.html(캔버스 보드) |
| 앱 화면 | docs/design/app/README.md |
| 메일 엔진 설정 · 운영 배포 · 백업 · 감시 · 전환 | mail/README.md |
| 앱 개발 실행 · 사내 배포(서명 키 · APK 올리기) | flutter/README.md |
| CI 러너 | ci/README.md |
| 개발 규칙(공통 · 스택별 · 역할) | CLAUDE.md · 아래 "개발 규칙" |
| 로드맵 | docs/roadmap.md |
| 로고 · 색 | 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)
빠른 시작 (개발)
# 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
- 웹 http://localhost:5173 · API http://localhost:3000/api · Swagger http://localhost:3000/api-docs
- 의존성을 바꿨을 때만
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.
앱 실행 (Flutter)
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.
환경변수
.env* 는 커밋하지 않는다. 각 자리의 *.example 을 복사해 쓴다. **루트 .env.{env} 가 단일 진실 공급원(SSOT)**이다 — backend · frontend 는 compose 가 루트 값을 넣어 준다
(구조는 .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 "백업").
표준 응답 포맷
// 성공
{ "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.
커밋 전 검사
# 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
컨테이너 이름은 <PROJECT_NAME>-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/rules/ |
| 응답 · 에러 | response-format.md · error-codes.md |
| API 설계 | api-design.md |
| 로깅 · 보안 | logging.md · security.md |
| 점검 목록 | checklist.md |
| NestJS | .claude/skills/nestjs/SKILL.md |
| Vue 3 | .claude/skills/vue3/SKILL.md |
| Flutter | .claude/skills/flutter/SKILL.md |
| Docker · 운영 | .claude/skills/docker/SKILL.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(역할) · 설정