# Relay 백엔드 연동 진행 현황 > 프론트 UI(목업)를 NestJS 백엔드에 단계적으로 연동하는 작업의 진행 기록. > 계약/스키마 상세는 [`api-contract.md`](./api-contract.md) 참조. > 최종 갱신: 2026-06-16 · 현재 위치: **2단계(저장소) 완료 + Gitea 연동, 3단계(멤버) 대기** --- ## 1. 작업 전략 - **도메인별 수직 슬라이스**: 한 도메인을 `엔티티 → DTO → service → controller → Swagger → 프론트 composable → store → 페이지 목업 교체`까지 끝내고 다음 도메인으로 이동. (수평 분할 아님) - 프론트는 백엔드 원시 데이터(타임스탬프/카운트)를 받아 **표시용 파생값(상대시간·진행률·아바타 색상 등)은 `shared/utils/format.ts`에서 계산**. - 모든 응답은 표준 래퍼 `{ success, data }` / `{ success, error }`. 에러 코드 맵은 `CLAUDE.md` SSOT. - 인증은 **이메일+JWT만 먼저**(소셜 로그인·이메일 인증은 8단계로 보류). access/refresh 모두 HttpOnly·Secure 쿠키. ### 단계 순서 ``` 0. 계약 정렬 ✅ → 1. 인증 ✅ → 2. 저장소 ✅ → 3. 멤버 → 4. 업무 → 5. 업무 상세 부속 → 6. 활동 피드 → 7. 내 업무 → 8. 마무리 ``` --- ## 2. 완료된 단계 ### 0단계 — 계약 정렬 ✅ - **`docs/api-contract.md`**: 7개 도메인 전체 엔드포인트(요청/응답/권한), ID 전략, 파생 필드 규칙, 상태 머신, 비즈니스 에러 메커니즘을 단일 진실 공급원으로 정의. - **비즈니스 에러 메커니즘**: `backend/src/common/exceptions/business.exception.ts` 의 `BusinessException(code, message, status)`. - 전역 필터(`http-exception.filter.ts`)가 `{ code, message }` 를 status 매핑보다 **우선** 처리 → 정확한 HTTP 상태를 유지하면서 사용자에게 구체 문구 노출. - 프론트 `useApi` 인터셉터는 `BIZ_001` 코드일 때 서버 `message` 를 우선 표시. - 기존 status→코드 매핑 구조는 그대로 유지(덧댄 우선 분기). `BusinessException` 을 쓰지 않으면 기존과 동일 동작. ### 1단계 — 인증 (이메일+JWT) ✅ **백엔드** `backend/src/modules/auth/`, `modules/user/` - `User` 엔티티(UUID, email unique, `password_hash` select:false, name, role nullable). - `UserService`(Repository 기반): `findById`/`findByEmail`/`findByEmailWithPassword`/`create`/`toPublic`. - `AuthService`: 가입(중복 검사), 로그인 검증(bcryptjs), access/refresh 토큰 발급. - `AuthController`: `POST /auth/signup`·`/login`·`/logout`·`/refresh`(refresh 가드), `GET /auth/me`(access 가드). HttpOnly 쿠키 발급/제거. - JWT access/refresh 전략(쿠키에서 추출) + 가드 + `@CurrentUser()` 데코레이터. - `main.ts` 에 `cookie-parser`. JWT 시크릿은 `backend/.env.{env}`(커밋된 `.example` 포함). **프론트** `frontend/src/` - `types/auth.ts`, `composables/useAuth.ts`, `stores/auth.store.ts`(bootstrap/login/signup/logout). - `composables/useApi.ts`: 401→`/auth/refresh` 자동 1회 재시도, `silent` 플래그(부트스트랩 무알림), BIZ_001 서버 메시지 우선. - 라우터 가드: 최초 진입 시 `/auth/me` 세션 복원 + 미인증 리다이렉트(redirect 쿼리 보존). - LoginPage/SignupPage 실제 연동(소셜 버튼은 안내, 가입 즉시 자동 로그인), AppShell 아바타=실사용자 + 로그아웃 드롭다운. **로그인 실패 문구**: `BusinessException('BIZ_001', '이메일 또는 비밀번호가 올바르지 않습니다.', 401)` / 중복 가입: 동일 패턴 409. ### 2단계 — 저장소(Repo) CRUD ✅ **백엔드** `backend/src/modules/repo/` - `Repo` 엔티티: `slugName`(영문, 라우팅 id, unique), `owner`('marketing-team' 고정 상수), `name`(한글 표시제목), `description`, `visibility`, `branch`. - `RepoMember` 엔티티: repo↔user 조인, `roleType`(admin/member), `isOwner`, `subRole`, `@Unique([repo, user])`. - `RepoService`: 내 멤버십 저장소 목록 / 단건(멤버·공개 접근 체크) / 생성(생성자를 owner·admin 등록) / 수정(admin) / 삭제(owner). - `GET/POST/PATCH/DELETE /repos[/:repoId]` — 전부 `JwtAuthGuard`. 응답 `RepoResponse`(원시값; `doneCount/totalCount`=0, 4단계에서 집계). **프론트** - `shared/utils/format.ts`: `avatarColor`(id 해시)·`initialOf`·`relativeTimeKo`·`progressOf` 추가. - `types/repo.ts`, `composables/useRepo.ts`, `stores/repo.store.ts`(API→기존 mock `Repo`/`User` 뷰로 매핑: 아바타 3명+`moreCount`, 진행률, 상대시간). - 페이지 연동: **RepoListPage**(목록·로딩/빈상태), **RepoCreatePage**(생성→상세), **RepoDetailPage**(헤더·진행률만), **RepoSettingsPage**(저장·삭제·이름변경 안내). ### 2.5단계 — Gitea 조직 연동 ✅ **백엔드** `backend/src/modules/gitea/` - `GiteaService`: 외부 Gitea(`/api/v1`) 얇은 클라이언트(Node 내장 `fetch`, 새 의존성 없음). `createRepo`/`updateRepo`/`deleteRepo` + `enabled` 게이트. `GiteaApiError`(상태코드 보존). - 환경변수 `GITEA_BASE_URL`/`GITEA_TOKEN`/`GITEA_ORG` 셋 다 있어야 활성화. 비어 있으면 저장소 CRUD 가 Gitea 호출 없이 기존대로 동작(로컬/미연동 대비). - `Repo` 엔티티에 연동 컬럼 추가: `giteaRepoId`, `giteaFullName`, `cloneUrl`, `htmlUrl`(전부 nullable). `RepoResponse` 에 `cloneUrl`/`htmlUrl` 노출. - `RepoService` 와이어링: - **생성**: Gitea 조직에 repo 생성(`auto_init`) → 성공 시 Relay 저장(연동정보 보관). 소유자=`GITEA_ORG`. 이름 충돌(409)→`BIZ_001`. Relay 저장 실패 시 Gitea repo 롤백 삭제. - **수정**: 표시제목/설명→Gitea description, 공개범위→private 동기화(slug 불변). 베스트 에포트. - **삭제**: Gitea repo 삭제 동기화. 베스트 에포트(실패해도 Relay 삭제 유지, 에러 로그). - 인프라 변경 없음(외부 Gitea). 토큰은 JWT 처럼 `backend/.env.{env}` 의 백엔드 고유 시크릿. 커밋용 `backend/.env.{env}.example` 에 키 문서화. > **TODO(프론트)**: RepoSettingsPage/RepoDetailPage 에 `cloneUrl`/`htmlUrl` 표시(클론 주소 복사, Gitea 열기 버튼). 백엔드 응답에는 이미 포함. --- ## 3. 남은 단계 ### 3단계 — 멤버 관리 - 백엔드: `GET/POST/PATCH/DELETE /repos/:repoId/members`(목록·초대·역할변경·제거), admin 권한 가드, owner 제거 불가. - 프론트: `useMember`/`member.store`, **RepoMembersPage**(초대 모달·역할 변경·제거) 연동. - `RepoMember` 엔티티는 2단계에서 이미 존재 → 데이터 계층은 일부 재사용. ### 4단계 — 업무(Task) CRUD + 상태 전이 - 백엔드: `Task`(+`ChecklistItem`) 엔티티, `GET/POST/PATCH/DELETE /repos/:repoId/tasks[/:taskId]`, `POST .../status`. 상태 머신 todo→prog→review→done/changes. - 저장소 `doneCount/totalCount` 실제 집계로 교체(현재 0 하드코딩). - 프론트: **RepoDetailPage 업무 목록**, **TaskCreatePage**, **TaskDetailPage**(지시자), **TaskAssigneePage**(담당자) 연동. ### 5단계 — 업무 상세 부속 - 체크리스트 토글, 댓글/답글, **파일 첨부**, 승인 워크플로우(승인 요청/승인/수정 요청). - **결정 필요**: 파일 저장소(로컬 볼륨 vs S3/MinIO). ### 6단계 — 활동 피드 - 2~5단계 행위를 이벤트로 적재(쓰기 API 없음, 읽기만). `GET /repos/:repoId/activity`. - 프론트: **RepoActivityPage**, TaskDetail 활동 탭. - **결정 필요**: 활동 표현 — 현재 mock은 HTML 통문자열 → 구조화(타입+행위자+대상) 권장. ### 7단계 — 내 업무 집계 - `GET /tasks/assigned`·`/tasks/issued`(저장소 횡단 집계). 프론트: **MyTasksPage**(담당/지시 탭). ### 8단계 — 마무리(선택) - 소셜 로그인(Google/Kakao), 이메일 인증, 알림, AI 에이전트 패널, Redis 캐싱, 전체 페이지네이션 통일. --- ## 4. 전환기 한계 (현재 시점) - **RepoMembersPage·RepoActivityPage** 는 아직 mock `findRepo` 사용 → 실제 생성한 저장소의 **멤버/활동 탭은 "찾을 수 없음"** 표시(각각 3·6단계에서 해소). - **RepoDetail 업무 목록**도 mock(4단계). 업무 모듈이 없어 모든 저장소 **진행률은 `0% / 시작 전`**. - `src/mock/relay.mock.ts` 는 아직 타입 정의(`Repo`/`User`/`TaskStatus` 등) + 미연동 화면 데이터 공급원으로 사용 중. 해당 도메인이 연동되면 데이터는 제거하고 타입만 `src/types/` 로 이전. --- ## 5. 알아둘 사항 / 결정 기록 - **엔티티 순환참조**: 서로 참조하는 관계(예: `Repo`↔`RepoMember`)의 **단일 클래스 관계 속성에는 TypeORM `Relation<>` 래퍼**를 사용한다. 안 그러면 SWC 환경에서 `Cannot access 'X' before initialization` 크래시. → 4·5단계(Task↔Repo, Comment↔Task 등)에도 동일 적용. - **bcrypt → bcryptjs**: Docker 베이스가 `node:20-alpine`(빌드 도구 없음)이라 네이티브 모듈 `bcrypt` 대신 순수 JS `bcryptjs` 사용. - **dev 도커 익명 볼륨**: `/app/node_modules` 가 익명 볼륨이라 **의존성 변경 시 `up -d --build -V`(익명 볼륨 갱신)** 필요. `--build` 만으로는 기존 node_modules 볼륨이 재사용되어 새 패키지가 반영되지 않음. - **owner 고정**: 조직 모델이 없어 `owner='marketing-team'` 상수. **Gitea 연동 시 owner=`GITEA_ORG`** 로 대체(미연동 시 상수 폴백). 조직 도입 시 교체. - **ID 전략**: User/Repo PK=UUID, Repo 라우팅 식별자=`slugName`, Task=저장소 내 순번(#9, 4단계). - **Gitea 연동 방식**: HTTP 클라이언트는 새 의존성(@nestjs/axios) 없이 Node 20 내장 `fetch` 사용(베이스 이미지 alpine 호환). 인증은 조직 토큰 1개(사용자별 Gitea 계정 매핑은 8단계 후보). `slug`↔Gitea repo name 1:1, 한글 표시제목은 Gitea description 에 합쳐 저장(Gitea 에 표시명 필드 없음). - **연동 비활성 폴백**: `GITEA_*` 미설정 시 `GiteaService.enabled=false` → 저장소 CRUD 가 Gitea 없이 동작. 로컬 개발/테스트에서 Gitea 없이도 기존 흐름 유지. --- ## 6. 실행 / 검증 ```bash # 개발 환경 기동 (의존성 변경 시 --build -V) docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d --build -V # 백엔드 로그 docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development logs -f backend ``` - 백엔드: `npm run build` / `npm run lint` (현재 0 error) - 프론트: `npm run type-check` / `npm run lint` / `npm run build-only` (현재 0 error) - dev `synchronize=true` 가 `users`·`repos`·`repo_members` 테이블 자동 생성(Gitea 연동 컬럼 포함). DB가 비어 있으므로 **회원가입 후 로그인**. - 포트: 프론트 5273, 백엔드 3100(`/api`), Swagger `http://localhost:3100/api-docs`. ### Gitea 연동 설정/검증 1. `backend/.env.{env}` 에 `GITEA_BASE_URL`/`GITEA_TOKEN`/`GITEA_ORG` 채우기(예시: `backend/.env.{env}.example`). - 토큰: Gitea → Settings → Applications → Generate Token(조직 repo 생성 권한 필요). - docker-compose 는 `backend/.env.{env}` 를 `env_file` 로 자동 주입하므로 compose 수정 불필요. 2. 부팅 로그에서 `Gitea 연동 활성화 — (org: )` 확인(미설정 시 비활성 경고). 3. Relay 에서 저장소 생성 → Gitea `` 조직에 동일 slug repo 가 생기는지 확인. 응답 `cloneUrl`/`htmlUrl` 채워짐. 4. 수정(설명/공개범위)·삭제 시 Gitea 측 반영 확인.