목차 › 5. 기술 스택 및 개발 환경
기술 스택 및 개발 환경
무엇을 썼는가보다 왜 그것이어야 했는가를 남긴다 · 기준 시점 저장소
Seuk-Team/Arda 8071fd9 · 최종 갱신 2026. 09. 22.
1. 기술 스택 선정 및 근거
기술 · 범위 · 윤리 결정 36건을 ADR(docs/03_decision/)로 남겼다.
안 한 것에도 ADR 이 있다 — Kubernetes 제외(0001) · 실시간 공동편집
제외(0005) · SQS 워커 폐기(0036). 개정은 원문을 지우지 않고 절을 덧붙이므로,
뒤집힌 결정도 원래 근거가 그대로 남아 있다.
아래 표는 2026. 09. 22. 기준 현행 스택이고, 이어지는 카드가 갈림길이 있었던 항목의 근거다.
| 영역 | 스택 | 근거 파일 |
|---|---|---|
| 백엔드 | Python 3.12 · FastAPI · SQLAlchemy 2.0 · psycopg 3 ·
PostgreSQL 16 + pgvector(pgvector/pgvector:pg16) ·
alembic · uv(의존 잠금) · pytest + pytest-timeout(60초) ·
ruff --select F |
backend/pyproject.toml |
| 아키텍처 | 헥사고날 부분 적용 — Bounded Context 4개(hiring · talent ·
application · interview) + ports/output 포트 ·
adapter/outbound/{pg,llm,mail} 어댑터 (ADR-0035) |
backend/app/ |
| 프론트엔드 | React 19 · Vite 8 · TypeScript 6 · react-router 7 · three.js(아르 3D 캐릭터) · CSS Modules + 디자인 토큰 · oxlint | frontend/app/package.json |
| 앱 | Flutter(CI 3.44.8 · Dart SDK ^3.12) · http · flutter_secure_storage(토큰은 Android Keystore) · Android APK | mobile/pubspec.yaml |
| AI · 판단 | Anthropic Claude(claude-haiku-4-5) — 서류 요약 · 평가 ·
추천 3단 체인(ADR-0022) · 아르 도구 호출 에이전트 · 지원자 FAQ.
규칙 의도 라우터가 빈출 요청 · 기본 질문을 LLM 없이 처리($0) |
backend/app/agent/ |
| AI · 검색 | sentence-transformers(ko-sroberta) 임베딩 + pgvector 시맨틱 검색 (ADR-0021) | backend/app/agent/embedder.py |
| AI · 음성 · 표정 | OpenAI Whisper API(whisper-1) STT — STT_BACKEND 로
로컬 faster-whisper 전환 가능(ADR-0038) · lie-detection 서비스(Python 3.13 ·
MediaPipe 얼굴 랜드마크 · ViT 표정 · librosa) —
담당자 화면 참고 지표 전용, 점수 미반영(ADR-0029 · 0032) |
ai/lie-detection/ |
| AI · R&D 자산 | Qwen3-8B QLoRA 어댑터 3갈래(chat v9 · summary · interview) —
동일 채점기로 학습 전 26.1% → v9 73.9%. 심사 서빙엔 쓰지 않고
AGENT_*_BACKEND=ollama 스위치로 교체 가능
보존 |
ai/qwen-training/ |
| 무결성 | web3 · Ethereum Sepolia 테스트넷 앵커 · OpenTimestamps — 제출물 SHA-256 해시 사슬을 추가 전용 원장에 쌓고 DB 트리거가 수정 · 삭제를 거부(ADR-0028). 서명 개인키는 서버 밖(GitHub Actions) | backend/app/chain.py |
| 메일 | n8n 웹훅 + SMTP — n8n 이 죽어도 API 가 5분마다 밀린 메일을 재발송(최대 3회). SES · SQS 는 폐기 폐기 (ADR-0030 · 0031 · 0036) | backend/app/shared/mail_smtp.py |
| 인프라 | Docker Compose(db · api · caddy · lie-detection · n8n) · AWS EC2 t3.medium(서울 · Elastic IP · EBS 50GB) · S3(presigned URL 직접 업로드) · Caddy(443, 인증서 자동) · Vercel(프론트) · systemd 타이머 자동 CD · CloudWatch + SNS 경보 · 매일 S3 DB 백업(30일 수명주기) | infra/ · 07-deploy |
| CI | GitHub Actions 5잡 — ① 백엔드 린트(ruff F) ② 백엔드 테스트(alembic 이행 → 모델↔이행 비교 → pytest) ③ 프론트 빌드·린트 ④ AI 면접 서버 시험 ⑤ 앱 시험 | .github/workflows/ci.yml |
갈림길이 있었던 것 — 무엇과 비교했고 무엇을 기준으로 잘랐는가
백엔드 — Python · FastAPI
선정 ADR 없음 · 남은 근거는 결과 쪽이다FastAPI 자체에는 비교 ADR 이 없다. 기획 단계에서 스택 표에 그대로 적혀 시작했고, 갈림길로 기록된 것은 그 위의 프론트(ADR-0006)와 앱(ADR-0010)이다. 지어내지 않고 실제로 값을 한 자리만 적는다.
첫째, 계약의 진실이 코드에서 나온다. Swagger /docs 가 라우트 107개의
요청 · 응답을 자동 생성하고, 02-api.md 는 "무엇이 있는가"만 유지한다.
프론트 · 앱 두 클라이언트가 이 문서 하나로 병렬 개발했고,
배포본 OpenAPI 를 main 과 대조하는 스크립트(check_public_contract.py)가
08/31 실측에서 35/35 · 62/62 일치를 확인했다.
둘째, 에이전트가 같은 런타임 안에 있다. backend/app/agent/ 는 별도
서비스가 아니라 같은 Python 프로세스이고, Claude SDK · sentence-transformers ·
pypdf 가 API 코드와 한 의존 묶음에 들어간다. 에이전트가 백엔드에 제공하는 요약 생성
함수도 프로세스 안 호출이라 두 오너 간 인터페이스가 HTTP 가 아니라 함수 시그니처다.
셋째, 그 한 덩어리가 커지자 헥사고날 부분 적용으로 Bounded Context 4개와
adapter/outbound/{pg,llm,mail} 을 갈랐다(ADR-0035). 지금 모델 ·
메일 · 큐 · 스토리지 공급자를 환경변수 스위치
(AGENT_CHAT_BACKEND · MAIL_DISPATCH ·
QUEUE_BACKEND · S3_ENDPOINT_URL)로 갈아 끼울 수 있는 것이
그 재편의 결과다.
프론트엔드 — React, 서버 템플릿(Jinja) 기각
ADR-0006 · 확정 2026. 08. 23.목업이 순수 정적 HTML 이라 두 갈래가 가능했다. Jinja 는 FastAPI 가 HTML 을 완성해 내려주므로 프론트 서버 · 빌드 · API 호출 · CORS 가 통째로 없다. React 는 프로젝트를 따로 세우고 팀원 HTML 을 JSX 로 다시 쓴다.
이식 비용은 쟁점이 아니었다 — HTML → JSX 는 기계적 변환이다. 실제 무게는 딸려오는 층이었다: 개발 서버 두 개 · 빌드 · CORS · 배포 대상 두 곳. 그 층이 막히면 데모 자체가 안 뜬다. 그래서 판단 기준을 하나로 잡았다 — "프론트가 안 뜰 때 혼자 고칠 수 있는가."
React 를 택했다. 그 층들이 이미 손에 익어 있었고, 익숙하지 않은 스택을 새로 배우는 비용이 그 층을 유지하는 비용보다 크다고 봤다. 딸린 결정 하나 — 공통 조각(사이드바 · 테이블 행 · 버튼 · 입력)을 먼저 컴포넌트로 만든 뒤 화면을 이식한다. 안 그러면 화면마다 같은 마크업을 네 번 다시 친다.
앱 — Flutter, Expo 에서 재확정
ADR-0010 · 08. 25. Expo 1차 확정 → 08. 26. Flutter 재확정학습 비용은 판단 기준으로 삼지 않았다. AI 코딩 보조를 쓰면 새 언어의 문법 학습 비용이 예전만큼 크지 않고, Dart 를 처음 본다는 것이 그 자체로 Flutter 를 기각할 근거가 못 된다. 기준은 "팀 폰에서 돌아가는 데모를 09/30 까지 만들 수 있는가" 하나였다.
08. 25. 에는 이 기준이 Expo 를 가리켰다. 완료 기준이 "팀 전원 기기에서 실행 확인" 이었고 팀 기기가 iOS 1대 · Android 4대라, Mac · Xcode 가 필요한 Flutter 로는 그 1대를 올릴 수 없었기 때문이다. 08. 26. 에 팀이 데모 범위를 좁혀 iOS 1대를 시연 대상에서 뺐고, 그 순간 Expo 를 골랐던 유일한 근거가 사라졌다. 바뀐 것은 기술 비교가 아니라 데모 범위다.
대신 감수한 것을 숨기지 않고 적었다 — 팀에 Dart 사용자가 0명이고 앱은 1인
트랙이라 막히면 혼자 푼다. 팀 배포는 매번 APK 를 만들어 전달한다(Expo Go 의 QR 즉시
반영이 없다). iOS 호환은 끝까지 미검증으로 남는다 — iOS 지원 플러그인만 쓰고
Platform.isAndroid 분기를 넣지 않지만, Mac 이 없어
flutter build ios 를 실행해 본 적이 없다. 그래서 발표 문구까지 고정했다:
"iOS 도 됩니다"가 아니라 "포팅 가능한 상태로 뒀고, 빌드 검증은 장비가 없어 못 했습니다."
패키지도 같은 방식으로 골랐다. http — dio 는 인터셉터 · 재시도가
필요할 때 값을 하는데 서버에 리프레시 토큰이 없어 갱신 로직이 없고 처리할 것이 401
하나뿐이다. flutter_secure_storage — shared_preferences 는 평문이라
쓰지 않는다, 12시간짜리 토큰이라도 남의 계정이다.
url_launcher — 이력서를 앱 안에서 보려면 PDF 렌더러가 또 필요하고
이미지 · docx 는 각각 다르다, 서버가 준 presigned URL 을 브라우저로 넘긴다.
스키마 이행 — 손 SQL 에서 alembic 으로
2026. 09. 01. 전환 · 사고가 먼저 있었고 도구는 그 뒤에 왔다
전환 이유는 취향이 아니다. create_all 은 없는 테이블을 만들 뿐
기존 테이블에 컬럼을 붙이거나 타입을 넓히지 않는다. 2026. 09. 01. 하루에 그 대가를
두 번 치렀다.
· 로컬 DB 의 users.role 체크 제약이 옛 값이라 테스트 101건이 error
· 운영 applications.ai_summary_model 이 varchar(50) 이라
AI 요약이 전건 실패 — Claude 호출 3번을 다 끝낸 뒤 저장에서 죽어
돈은 쓰고 결과는 버렸고, BackgroundTasks 라 화면에 에러도 안 떴다
두 번 다 "각자 손으로 SQL 을 돌려라"로 수습했다. 코드는 pull 로 따라오지만 DB 는
안 따라온다 — 문서에 손 SQL 을 적는 방식은 사람마다 스키마가 갈리는 것을 막지
못한다. 지금은 리비전 26개가 쌓여 있고 uv run alembic upgrade head 한 줄로
따라온다.
도입 자체도 한 번 뒤집었다. 처음에는 uv run --with alembic 으로 잠금 파일을
건드리지 않았다 — Dockerfile 의 uv 핀과 uv.lock 형식이 어긋나 이미지
빌드가 깨질까 봐 피한, 근거 있는 판단이었다. 뒤집은 이유는 버전이 아무 데도 안 박힌다
는 것이다: --with 는 매번 최신을 받으므로 사람마다 · CI 마다 다른
alembic 으로 같은 리비전을 돌리게 된다. 그리고 걱정했던 위험은 실측해 보니 성립하지
않았다(잠금 파일 형식 그대로, 기존 패키지 버전 이동 0건, 두 줄만 추가).
원래 판단이 틀렸던 게 아니라 전제가 이미 바뀌어 있었다.
이후 alembic 은 dev 묶음에서 운영 의존으로 올라갔다 — 배포가 컨테이너 안에서
이행을 돌리므로(2026. 09. 04. 이슈 #17), dev 그룹에 두면 --no-dev 빌드에서
빠져 이행이 아예 불가능하다.
인프라 — Kubernetes 제외, AWS 8종 → 3종
ADR-0001 · ADR-0031 · ADR-0036K8s 는 뺐다. 한 달 안에 K8s 를 "굴렸다"고 말하려면 배포 전략 · 오토스케일링 · 헬스체크 튜닝 중 최소 하나는 실제로 다뤄야 하는데, 서비스 기능까지 만들면서는 매니페스트 쓰고 파드 뜨는 수준이 한계다. 그 수준은 면접에서 파고들면 방어가 안 된다. 그래서 Docker + AWS + GitHub Actions CI/CD 까지를 목표로 잡았다 — "못 만들어서가 아니라, 한 달 규모에서 설명 가능한 깊이까지 못 가기 때문에 뺐다."
AWS 는 8종에서 3종(EC2 · S3 · IAM)으로 줄였다. 이유는 기술이 아니라
기한이다 — 예산이 $400 · 2026. 10. 27. 까지인데 그 뒤에도 서비스가
돌아야 한다. 나머지 다섯(SES · SQS · CloudWatch · SNS ·
CloudFormation)은 self-host 대안으로 갈아 끼울 스위치를 만든 뒤 폐기 대상으로
돌렸고(콘솔에서 지우는 것은 10. 27. 이후다), 메일은 n8n + SMTP 로 이관했다. 주장으로 두지 않고
PC 한 대에 운영과 같은 스택을 통째로 띄워 실증했다(infra/local/, 2절).
모델 — 자체학습 Qwen 과 Claude 를 같은 자로 재고 클라우드 확정
ADR-0024 → 2026. 09. 18. 확정
Qwen3-8B 를 QLoRA 로 직접 학습해 어댑터 3갈래(chat · summary · interview)를
만들었고, 동일 채점기(judge.py) · 동일 23건으로 쟀다 —
학습 전 26.1% → Claude Haiku 69.6% → Qwen v9 73.9%.
정확도는 동급까지 왔다.
그런데도 심사 서빙은 클라우드 + Claude 로 확정했다. 갈린 것은 정확도가 아니라
수치 추론 · 형식 안정성 · 콜드 스타트 78초 · 동시성이다.
실제로 학원 PC 3대로 온프레미스를 구축해 운영까지 해 봤고,
도구 오호출 · 프롬프트 예시 숫자 베끼기 · JSON 따옴표 깨짐이 심사 직전에
누적돼 폐쇄했다. 학습 파이프라인과 어댑터는 버리지 않고
AGENT_*_BACKEND=ollama 스위치로 교체 가능한 R&D 자산으로 보존했다.
2. 개발 환경 구성
로컬 환경의 원칙은 하나다 — 기본값이 곧 기존 동작이고, 로컬 초록과 CI 초록이
갈리지 않게 한다. 로컬 전용 우회를 허용하면 "내 PC 에선 됐는데"가 배포까지 간다.
아래 함정 목록은 전부 실제로 겪은 것이고, 겪을 때마다
docs/00_overview/08-local-setup.md 에 쌓았다.
평소 셋업 — 네 줄
순서를 2026. 09. 02. 에 바꿨다. 전에는 alembic 이
docker compose up 앞에 있었는데, DB 가 떠 있지 않으면 마이그레이션이 붙을
데가 없다. 컨테이너가 하나도 없는 PC 에서 그대로 따라 하면 거기서 막힌다 —
실제로 겪고 고친 순서다.
로컬 compose 가 띄우는 것은 넷이다: db(pgvector/pgvector:pg16,
5432) · api(8000) · minio(9000) ·
minio-init. DB 이미지는 CI · 운영과 같은 것을 쓴다 —
순정 postgres 에는 vector 확장이 없어 임베딩 경로가 통째로 안 돈다.
백엔드 스위치 — 아무것도 설정하지 않으면 지금까지와 동일하게 돈다
| 환경변수 | 기본 | 로컬로 돌리려면 | 비고 |
|---|---|---|---|
STT_BACKEND |
백엔드 openai (빈 값도) ·
실시간 면접 서버는 빈 값이면 로컬 |
faster_whisper
(uv sync --extra local 필요) |
같은 변수, 두 해석 — 같은 backend/.env 를 두 컨테이너가
다르게 읽는다. 로컬에서 비워 두면 백엔드는 API 로, 실시간 서버는 CPU 로 받아쓴다
(ADR-0038) |
AGENT_CHAT_BACKEND |
anthropic |
ollama |
어댑터 유닛 테스트는 mock 기반이라 Ollama 없이 돈다. 실제 추론이 필요한 사람은 에이전트 오너뿐 |
AGENT_SUMMARY_BACKEND |
anthropic |
ollama |
GPU 가 없으면 OLLAMA_HOST 로 GPU 장비 한 대를 가리킨다 —
포트를 열지 말고 SSH 터널 |
모르는 값을 넣으면 조용히 폴백하지 않고 즉시 예외로 죽는다. 오타 하나로 개인정보가 외부로 나가면 안 되기 때문이다 — 폴백이 친절해 보이지만, 이 경우엔 틀린 설정이 성공처럼 보이는 것이 더 비싸다.
자주 물리는 함정 — 코드가 아니라 내 환경이 원인인 것들
5432 를 네이티브 PostgreSQL 이 선점
컨테이너 DB 가 정상 기동해도 연결이 그쪽으로 간다. 옛 스키마의 다른
arda DB 에 붙어 DB 의존 테스트가 통째로 error 난다.
개인 오버라이드로 다른 포트를 함께 매핑해 쓴다.
pytest 가 backend/.env 를 읽는다
2026. 09. 07. 부터. 전에는 .env 에 다른 포트를 적어도 무시되고 5432 로
붙었다. 단 에이전트 스위치는 여전히 빈 값으로 선점된다 —
.env 에 실제 키가 있어도 테스트엔 안 들어간다.
로컬 초록 · CI 빨강이 갈리는 것을 막기 위해서다.
MinIO 버킷 이름
로컬 S3 는 arda-local 버킷을 만든다. S3_BUCKET 을 실 AWS
이름으로 둔 채 업로드하면 전건 404 로 죽는데, 그 404 는 브라우저가 S3 로 직접
PUT 하다 나는 것이라 API 로그에 안 남는다.
요약 · 임베딩은 응답 뒤에 돈다
BackgroundTasks 라 접수 API 가 201 을 줘도 요약은 아직이다.
실패해도 화면에 안 뜬다 — DB 에서 건수를 세서 확인해야 한다.
WSL 에서 vite 가 변경을 못 본다
2026. 09. 08. Windows 쪽에서 고친 파일이 inotify 로 안 올라와
서버를 켤 때의 코드가 계속 나온다. 고약한 것은 틀린 화면이 그럴듯하게
그려진다는 점이다 — 코드를 읽으면 맞는데 화면이 다르다.
VITE_DEV_POLL=1 로 폴링을 켜거나 개발 서버를 다시 띄운다.
.env 를 고쳐도 반영이 안 된다
.env 는 프로세스 시작 때 한 번만 읽는다. 그리고
docker compose restart 는 env_file 을 다시 읽지 않는다
— up -d --force-recreate 로 컨테이너를 다시 만들어야 한다.
실제로 이것 때문에 기능 하나가 "스위치가 없어서" 안 뜬 적이 있다.
운영 그대로 로컬 전체 스택 — infra/local/
위 네 줄은 개발용 최소 스택이다. 이와 별개로 운영(EC2)과 같은 스택을 PC 한 대에
그대로 띄우는 절차를 2026. 09. 08. 에 만들었다. 목적이 분명하다 —
"AWS 를 떠나도 돈다"(ADR-0031)를 주장이 아니라 사실로 만드는 것이다.
db · api · lie-detection · n8n · caddy 가 뜨고, 운영 compose 의
사본이라 차이는 TLS 없음 · 포트뿐이다. 서버에서 .env 두 벌 ·
pg_dump · n8n 볼륨을 받아 복원한 뒤 smoke.sh 가
한 번에 자동 검증한다 — 컨테이너 상태 · api 직접과 caddy 경유의
/health · 내부 API · 무결성 API 의 인증 게이트(401) ·
lie-detection · n8n Basic Auth 와 웹훅 · alembic 이 head 인지 ·
api 로그의 Traceback 0건.
스택 검증은 dev 서버(5173)가 아니라 프로덕션 번들 preview(4173)에서 한다 —
dev 모드에는 로그인 우회 · 목데이터 폴백이 켜져 있어 부적합하다.
가져온 것은 운영 데이터 사본이다 — 지원자 실제 이메일이 들어 있고
MAIL_DISPATCH=n8n 이라 단계를 바꾸면 진짜 메일이 나간다. 테스트는 이메일이
팀원 것으로 바뀐 지원자로만 한다.
CI 환경은 로컬 · 운영과 버전을 맞춰 못 박는다
버전이 떠 있으면 남의 커밋에서 CI 가 터진다. 그래서 전부 이유를 달아 고정했다 —
백엔드 Python 3.12(Dockerfile 의 python:3.12-slim 과 맞춘다) ·
프론트 Node 22(로컬 실측 v22.23.1 과 맞춘다) ·
AI 면접 서버 Python 3.13(ai/lie-detection/Dockerfile 과 맞춘다) ·
Flutter 3.44.8 · ruff 0.16.7(uvx 로 격리 실행 —
dev 묶음에 넣으면 uv.lock 이 바뀌어 전원이 uv sync 를 다시 돌려야 한다).
CI 의 DB 도 로컬 compose 와 같은 pgvector/pgvector:pg16 이다.
3. 형상 관리 및 브랜치 전략
저장소는 Seuk-Team/Arda(2026. 09. 04. 이관, 구 Team-Seuk/Arda 는
삭제). main 기준 커밋 1,095건 · 머지 커밋 297건(2026. 08. 20. ~ 09. 18.,
8071fd9)이다.
브랜치 전략은 두 번 바뀌었고, 두 번 다 사고나 병목이 먼저 있었다.
| 시기 | 규칙 | 왜 바꿨는가 |
|---|---|---|
| ~ 2026. 08. 27. | 이슈 → 브랜치 → PR → 사람 리뷰 · 승인 | 전 PR 을 팀장 한 명이 리뷰해, W1 실측에서 프로젝트 속도를 코딩이 아니라 팀장의 지시서 생산 · 리뷰 속도가 결정한다는 것이 드러났다(ADR-0007). 인터페이스 리뷰 · 머지가 기본 주 1회 일괄이라 선행 PR 이 밀리면 받는 쪽이 일주일을 논다. 도메인 오너제로 전환하면서 이 병목을 없애는 것이 목적이었다 |
| 08. 28. ~ 09. 03. | 승인 게이트 전면 폐지 — 전원 main 직접 push | 근거는 "전원이 Claude 로 작업해 사람 검수가 형식화됐다"였다.
대가가 바로 나왔다 — 09. 01. 하루에 깨진 main 을 두 번 발견했고
둘 다 우연이었다(d7fdd10 · d96749e) |
| 09. 04. ~ 현재 현행 | main 직접 push 금지 → 브랜치 → PR → 자체 머지 (CI 초록 필수, 승인 불요) | 승인 게이트는 여전히 없다 — 바뀐 것은 "main 에 닿는 경로가 PR 하나"
라는 점뿐이다. GitHub 브랜치 보호가 직접 push · force push 를 실제로 막으므로
규칙을 외울 필요가 없다 (818e693) |
전환이 숫자로 보인다. main 의 머지 커밋 297건 중 09. 04. 이전은 8건뿐이고 나머지 289건이 그 이후다. 그 전의 변경은 대부분 main 직접 push 였다는 뜻이다. PR 번호는 #354까지 갔다.
왜 승인 게이트를 되살리지 않았나. 08. 28. 폐지의 근거(사람 검수의 형식화)는 그대로 유효하다고 봤다. 되살린 것은 리뷰어가 아니라 경로다 — 안전망은 사람이 아니라 CI 초록이고, CI 빨간불 = 머지 불가가 오너제의 유일한 강제다. 대신 책임이 옮겨 갔다: main 머지 = 2분 후 프로덕션 배포이므로 main 이 항상 동작하는 상태여야 할 책임이 리뷰어에서 머지하는 사람에게 넘어왔다. 깨진 main 을 발견한 사람은 묻지 않고 고치거나(fix-forward) revert 한다 — main 이 빨간불이면 다른 작업보다 우선이다. 이의는 사후에 말하고, 합의가 안 되면 revert 가 기본값이다.
CI 5잡 — 게이트가 아니라 "깨졌다는 사실만 즉시 보이게"
| 잡 | 무엇을 | 왜 이 잡이 생겼는가 |
|---|---|---|
| 백엔드 린트 | ruff check --select F |
헥사고날 이동 뒤 미정의 이름 2건이 프로덕션 500 까지 갔다 —
그 줄을 타는 테스트가 없어 pytest 는 초록이었다.
--select F 만 켠다: 스타일까지 켜면 기존 파일 수백 줄이 빨개져
아무도 안 보는 빨간 CI 가 된다 |
| 백엔드 테스트 | alembic upgrade head → 모델↔이행 비교 →
pytest 1,165건 (실제 PostgreSQL + pgvector) |
모델↔이행 비교는 2026. 09. 17. 에 붙였다 — pytest 의 conftest 가
create_all 로 이행에 빠진 표를 채워 넣어 이행 누락이 CI 에서
가려지기 때문이다. alembic check 는 CHECK 제약의 값 목록을 안 봐서
09. 16.(메일 종류 password_setup 누락) ·
09. 17.(#282 file_blobs) 두 번 새어 나갔다. 리비전 번호가 겹치면
test_alembic_revisions.py 가 CI 에서 잡는다.
pytest-timeout 60초는 Docker 가 죽어 pytest 가 24분간 조용히
멈춘 사고에서 나왔다 — 멈춤을 침묵이 아니라 실패로 |
| 프론트 빌드 · 린트 | oxlint + tsc -b + vite build (Node 22) |
타입 · 린트 · 빌드가 한 잡에서 다 도는 것이 요점이다 — 빌드가 깨진 채 머지되면 Vercel 배포가 그대로 멈춘다 |
| AI 면접 서버 시험 | pytest 3파일 (Python 3.13) — 실시간 전사 · 판정 중계 · 화자 매칭 | 본체와 파이썬 버전 · 시스템 라이브러리(opencv · mediapipe · soundfile)가 달라 별도 잡으로 뗐다 |
| 앱 시험 | flutter analyze + flutter test —
테스트 파일 30개 (Flutter 3.44.8) |
ADR-0010 의 iOS 호환 규율 중 Mac 없이 가능한 검증이 analyze
무경고까지다. 그것만은 CI 가 대신 지킨다 |
같은 브랜치에 연달아 push 하면 앞의 실행을 취소한다(concurrency ·
cancel-in-progress) — 무료 러너 분을 아끼기 위해서다.
커밋 · 문서 · 공지 규약
| 항목 | 규칙 |
|---|---|
| 커밋 메시지 | <type>(<기능번호|도메인>): <한국어 요약> —
feat fix docs test
chore refactor.
예: feat(J7): 더미 지원서 10만 건 생성기 ·
fix(agent): 무관 질문 거절을 '데이터 근거' 기준으로.
리뷰가 없으므로 이력이 곧 기록이고, PR 본문에 무엇을 왜 바꿨는지 남긴다 |
| 검증 | 자기 파트 실행 · 테스트 결과를 커밋 메시지 한 줄로 남긴다. 테스트를 통과시키려고 주석 처리 · 우회 금지 — 실패하면 실패한 대로 적는다 |
| 스키마 변경 | 코드 + 01-erd.md + alembic 리비전을 같은 커밋에.
문서 갱신 없는 스키마 변경은 금지한다 — ERD 가 전원의 계약이기 때문이다 |
| API 변경 | 같은 커밋에서 02-api.md 갱신. 요청 · 응답의 진실은 Swagger 이고
02-api.md 는 "무엇이 있는가"만 유지한다 — 이중 관리하지 않는다 |
| 사후 공지 (한 줄) | 스키마 · API · 공용 문서(docs/00_overview/ ·
CLAUDE.md · .github/) ·
남의 도메인 폴더를 고쳤을 때. 큰 변경이면 직접 고치지 말고 이슈로 오너에게 넘긴다 |
| 시크릿 | .env(git 제외) + .env.example(키 이름만).
코드 · 커밋 · 로그에 키를 남기지 않는다 |
머지에서 배포까지 — 요청이 필요 없다
백엔드는 서버의 systemd 타이머(arda-deploy.timer)가 2분마다 main 을
폴링해 새 커밋이면 pull → build → alembic upgrade head →
up -d 를 순서대로 돈다(로그 ~/deploy.log).
프론트는 Vercel 이 1~2분 내 자동 배포한다.
스키마 이행은 api 이미지 안에서 돈다 — 2026. 09. 07. 에 호스트 마운트를 없애고
이미지가 alembic/ 을 갖게 했다(PR #20). set -euo pipefail 이라
이행이 실패하면 배포가 거기서 멈추고 기존 컨테이너는 계속 산다. 그래서
"배포해 주세요" 요청이 사라졌고, 동시에 CI 초록인 PR 만 머지해야 할 이유가
생겼다 — 머지가 곧 프로덕션이다.