# Relay 백엔드 연동 진행 현황 > 프론트 UI(목업)를 NestJS 백엔드에 단계적으로 연동하는 작업의 진행 기록. > 계약/스키마 상세는 [`api-contract.md`](./api-contract.md) 참조. > 최종 갱신: 2026-06-17 · 현재 위치: **0~4단계 완료(+ Gitea 양방향 연동·템플릿 복제·생성폼 보강·멤버 프론트 연동·멤버 초대 자동완성·import 멤버 데드락 해소·cloneUrl/htmlUrl·giteaMissing 배지·업무 CRUD/상태머신/진행률 집계), 5단계(업무 상세 부속) 대기** > 남은 프론트 연동: 업무 상세의 댓글/체크리스트 토글/첨부/승인 노트(5단계). 그 외 6~8단계 미착수. --- ## 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. 저장소 ✅ (+ Gitea 연동 ✅) → 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`, `@Unique([repo, user])`. - `RepoService`: 내 멤버십 저장소 목록 / 단건(멤버·공개 접근 체크) / 생성(생성자를 owner·admin 등록) / 수정(admin) / 삭제(owner). - `GET/POST/PATCH/DELETE /repos[/:repoId]` — 전부 `JwtAuthGuard`. 응답 `RepoResponse`(원시값; `doneCount/totalCount` 는 당시 0 하드코딩 → **4단계에서 업무 status 집계로 교체됨**). **프론트** - `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 생성 → 성공 시 Relay 저장(연동정보 보관). 소유자=`GITEA_ORG`. 이름 충돌(409)→`BIZ_001`. Relay 저장 실패 시 Gitea repo 롤백 삭제. (빈 저장소=`auto_init`, 템플릿=`generate` — 아래 생성폼 보강 참조) - **수정**: 표시제목/설명→Gitea description, 공개범위→private 동기화(slug 불변). 베스트 에포트. - **삭제**: Gitea repo 삭제 동기화. 베스트 에포트(실패해도 Relay 삭제 유지, 에러 로그). - 인프라 변경 없음(외부 Gitea). 토큰은 JWT 처럼 `backend/.env.{env}` 의 백엔드 고유 시크릿. 커밋용 `backend/.env.{env}.example` 에 키 문서화. > **프론트 ✅**: 공용 컴포넌트 `components/GiteaRepoBar.vue`(클론 주소 표시·복사 + 'Gitea에서 열기', 미연동 시 렌더 안 함)를 **RepoSettingsPage**(일반 카드)·**RepoDetailPage**(헤더 아래)에 적용. `Repo` 뷰 타입/`repo.store` 매핑에 `cloneUrl`/`htmlUrl`/`giteaMissing` 추가. ### 2.6단계 — Gitea → Relay 역방향 동기화 ✅ (백엔드) **백엔드** `backend/src/modules/gitea/`, `modules/repo/repo-sync.service.ts` - 문제: 동기화가 단방향(Relay→Gitea)이라 **Gitea 조직에 직접 만든 저장소가 Relay 목록에 안 떴음**. - `GiteaService.listOrgRepos()`: 조직 저장소 페이지네이션 순회(`GET /orgs/{org}/repos`). 결과에 `name`/`description`/`private` 추가. - `RepoSyncService`: **서버 기동 직후 1회(`OnApplicationBootstrap`, 비차단) + 매시간(`@Cron(EVERY_HOUR)`)** 동기화. `ScheduleModule.forRoot()` 는 `app.module` 에 이미 등록됨. - **추가형**: Gitea ID→slug 매칭. 없으면 import(멤버 0명), 있으면 연동정보만 backfill. 표시명/설명/공개범위는 import 후 Relay 가 SSOT. - **누락 표기**: Gitea 에서 사라진 연동 origin 저장소는 삭제하지 않고 `giteaMissing:true` 로만 표기(재출현 시 복구). `Repo.giteaMissing` 컬럼 추가, `RepoResponse`/프론트 `ApiRepo` 에 노출. - 미연동 시 건너뜀 + 중복 실행 가드. - **import 저장소 멤버 데드락 해소(2026-06-17)**: import 저장소는 멤버 0명이라 admin 이 없어 아무도 멤버를 초대할 수 없는 막힘이 있었음. 동기화 끝에 **`REPO_IMPORT_ADMIN_EMAIL`(env) 의 가입 사용자를 멤버 0명인 Gitea-연동 저장소에 owner-admin 으로 자동 지정**(신규 import + 기존 멤버 0명 모두 대상, Relay 생성분은 생성자가 이미 멤버라 제외). env 미설정/미가입이면 건너뜀(가입 후 다음 동기화에서 지정). 키 문서화: `backend/.env.{env}.example`. - **목록 모델 변경**: `GET /repos` 가 '내 멤버 저장소' → **'조직 저장소 전체'**(`findAll`). 단건(`findOne`)도 조직 모델상 인증 사용자 열람 허용(쓰기 권한은 기존 admin/owner 유지). 멤버십은 역할/권한 전용. > **프론트 ✅**: RepoListPage 에 `giteaMissing` '원격 없음' 배지(저장소 행 제목 옆) 표시. 헤더 'Gitea 연동됨' 배지는 실제 연동 여부(`htmlUrl` 보유)에 따라 노출되도록 보정. import 된 멤버 0명 저장소의 빈 아바타/0% 진행률은 정상. ### 3단계 — 멤버 관리 ✅ (백엔드 + 프론트) **백엔드** `backend/src/modules/repo/` - `MemberService` + `MemberController`(repo 모듈에 흡수, `RepoMember` 엔티티 재사용). 엔드포인트: `GET/POST/PATCH/DELETE /repos/:repoId/members[/:userId]`. - **목록**: 조직 모델상 **인증 사용자면 누구나 열람**(생성순 정렬, `RepoService.findOne` 과 동일 정책). 응답 `RepoMemberResponse { user, email, taskCount, roleType, owner }`. (※ 2.6단계 조직 모델 전환 후에도 list 가 옛 '멤버 or 공개' 체크를 유지해 import/타인 생성 저장소 멤버 탭이 403 나던 버그 수정 — 2026-06-17) - **초대**: admin 권한. **이미 가입된 사용자만 이메일로 초대**(별도 초대 메일/계정 생성 없음). 미가입 이메일→`BIZ_001`(404), 이미 멤버→`BIZ_001`(409). - **역할 변경·제거**: admin 권한. **소유자(owner)는 역할 변경·제거 불가**(`BIZ_001` 403). - `InviteMemberDto`(email/roleType), `UpdateMemberDto`(roleType?). 단위 테스트 `member.service.spec.ts`(소유자 보호·중복·권한 가드). - `taskCount` 는 4단계 업무 모듈 도입 전까지 0. - **Gitea 미동기화**: 인증 모델이 조직 토큰 단일 사용이라 멤버↔Gitea collaborator 동기화는 하지 않음(사용자별 Gitea 계정 매핑은 8단계 후보). **사용자 검색 API** `backend/src/modules/user/` ✅ (멤버 초대 자동완성용) - `GET /users?q=<검색어>`(`JwtAuthGuard`) — `q` 있으면 이메일/이름 부분일치(`ILike`, 대소문자 무시), 없으면 전체(이름순, 최대 50). 응답 `PublicUser[]`. - `UserController` 신규 + `UserService.search()` 추가, `UserModule` 에 컨트롤러 등록. (1단계에서 삭제했던 샘플 user.controller 와 무관한 신규 검색 전용) **프론트** `frontend/src/` ✅ - `types/member.ts`(`ApiRepoMember`/`InviteMemberPayload`/`UpdateMemberPayload`), `composables/useMember.ts`(list/invite/update/remove), `stores/member.store.ts`(API→화면용 `RepoMember` 매핑: 이니셜·색상 파생, `isYou`=현재 사용자 비교). - **RepoMembersPage** 목업 제거 → 실제 연동: 멤버 목록(로딩/빈 상태), 헤더·탭 카운트 실데이터, **초대 모달**(이메일+역할, 가입 사용자만, **사용자 자동완성** — 포커스 시 전체·입력 시 이메일/이름 부분일치, 기존 멤버 제외), **역할 변경 드롭다운**(멤버↔관리자), **멤버 제거**(confirm). 초대/역할변경/제거 컨트롤은 **현재 사용자가 admin 일 때만 노출**(`isAdmin`), 소유자(owner) 행은 변경·제거 불가. - 미가입/중복 초대 등은 인터셉터가 서버 `BIZ_001` 문구를 그대로 노출. `taskCount` 는 4단계까지 0. ### 4단계 — 업무(Task) CRUD + 상태 전이 ✅ (백엔드 + 프론트) **백엔드** `backend/src/modules/task/` - `Task` 엔티티: 저장소 종속(`@ManyToOne Repo`, `Relation<>`), `seq`(저장소 내 순번 #N, `@Unique([repo, seq])`), `title`, `content`(jsonb 문단 배열), `status`, `dueDate`, `issuer`(User, SET NULL), `assignees`(`@ManyToMany` → `task_assignees`), `checklist`(`@OneToMany ChecklistItem`, cascade). `ChecklistItem`: `text`/`done`/`sortOrder`. - `TaskService`: 목록(상태/담당자 필터, seq 내림차순) / 상세 / 생성 / 수정 / 삭제 / `changeStatus`. - **담당자 = 저장소 멤버만**: `resolveMemberAssignees` 가 `assigneeIds` 를 멤버십과 대조, 비멤버 포함 시 `BIZ_001`(400). - **순번 채번**: 저장소 내 `max(seq)+1`. - **상태 머신**: `todo→prog→review→done`, `review→changes`, `changes→{review,prog}`. 허용 외 전이는 `BIZ_001`(409). 역할: 승인/수정요청(review 출발)은 지시자/admin, 그 외 작업 전이는 담당자/지시자/admin. 비멤버는 전이 불가(403). - **승인 완료(→done) 부수효과**: 해당 업무의 체크리스트 항목 `done` 을 모두 true 로 **영속 처리**(승인=완료. `done` 은 출구 전이 없어 안전). 디자인 시안의 '승인 완료=체크리스트 100%' 동작 일치. - `TaskController`: `GET/POST/PATCH/DELETE /repos/:repoId/tasks[/:taskId]` + `POST .../:taskId/status`(`taskId`=seq, `ParseIntPipe`). 생성/수정/삭제는 admin. - **저장소 진행률 실집계**: `RepoService` 가 `TaskService.countsByRepo` 로 `doneCount/totalCount` 를 채운다(기존 0 하드코딩 제거). **멤버 `taskCount`**: `MemberService` 가 `TaskService.assigneeCounts` 로 사용자별 담당 수 집계(`RepoModule`→`TaskModule` import). 순환 모듈 없음(TaskModule 은 엔티티만 참조). - 단위 테스트 `task.service.spec.ts`(상태머신 위반·승인 권한·비멤버 차단·담당자 멤버 검증·**승인 시 체크리스트 일괄 완료**). 백엔드 18 tests pass. - `comments`/`activities`/`files`/`approval` 은 **5단계 영역** — 응답에 빈 배열로 내려간다. **프론트** `frontend/src/` - `shared/utils/format.ts`: `taskStatusLabel`·`taskDueInfo`(dueDate+완료여부→ dateLabel/dday/listLabel/overdue)·`monthDayKo` 추가. - `types/task.ts`, `composables/useTask.ts`(list/get/create/update/remove/changeStatus), `stores/task.store.ts`(API→mock `RepoTask`/`TaskDetail` 뷰 매핑: 마감/dday/상태 라벨 파생, 지시자 null 폴백). - 페이지 연동: **RepoDetailPage**(업무 목록 실데이터+로딩/빈상태, 툴바 ‘새 업무’ 버튼), **TaskCreatePage**(제목/내용 textarea/체크리스트/마감일/담당자=멤버 선택 드롭다운 → POST → 생성 상세로 이동), **TaskDetailPage**(상세 fetch, 삭제=DELETE, 승인=review→done·수정요청=review→changes), **TaskAssigneePage**(상세 fetch, 실제 status 로 카드 분기: 시작 todo→prog·승인요청 prog→review·재요청 changes→review, 삭제). - **업무 수정 UI**: `TaskCreatePage` 를 생성/수정 겸용으로 확장. 라우트 `/repos/:repoId/tasks/:taskId/edit`(`task-edit`) 추가 → `taskId` 있으면 편집 모드(기존 값 미리 채움 + `useTask().get` 로 원시값 로드 + 제출 시 PATCH `taskStore.update`). 헤더/버튼 라벨 전환(업무 수정/저장). 상세·담당자 페이지의 ‘수정’ 버튼을 `…/edit` 로 연결(기존엔 빈 생성 폼으로 오연결). **체크리스트 항목 편집은 5단계** — 수정 폼에선 표시·읽기전용(제목/내용/담당자/마감만 저장). - 댓글/AI 에이전트 패널/첨부 업로드는 **로컬 데모 유지(5단계 실연동)**. 담당자 페이지의 `?state=` 쿼리 의존 제거 → `task.status` 기반 분기. - **역할 기반 액션 통합(2026-06-17)**: 목록은 항상 `TaskDetailPage`(`…/tasks/:taskId`)로 진입하고, 이 페이지가 **현재 사용자 역할로 액션을 분기**한다. `isIssuer`(task.issuer===me) / `isAssignee`(me ∈ assignees) 계산 → 지시자는 승인 대기(review)에서 승인/수정요청, 담당자는 `업무 시작`(todo→prog)·`승인 요청`(prog→review)·`다시 승인 요청`(changes→review)·승인 대기 중(review) 표시. 담당자가 "업무 시작"에 도달할 경로가 없던 문제 해소(기존 시작 버튼은 `/view`(TaskAssigneePage)에만 있었고 거기로 가는 링크가 mock MyTasksPage뿐이었음). `TaskAssigneePage`/`/view` 는 mock MyTasks 전용으로 잔존(7단계 정리). **한계**: 지시자가 아닌 admin은 화면상 승인 버튼 미노출(편집/삭제는 가능, 백엔드는 admin 승인 허용). - **UI 정리(2026-06-17)**: 공용 `RepoHeader` 기본 ‘새 업무’ 버튼 제거(`#actions` 슬롯 제공 시에만 렌더) → 상세 헤더 중복 버튼 해소. 작성 폼 ‘임시저장’ 제거, 체크박스 토글 비활성(어느 화면도 사용자 체크 불가), 담당자 칩을 입력칸 아래로 분리, 지시자 직무 줄 제거. 저장소 목록의 진행률 라벨(‘시작 전/67% 완료’) 제거(업무 수·바는 유지). - **디자인 시안 정렬(2026-06-17)**: 담당자 액션 카드를 시안의 `.ap-card` 색상 체계로 정렬 — 승인요청(prog)=accent(`request`)·수정요청(changes)=빨강·승인완료(done)=초록·승인대기(review)=앰버. ‘업무 시작’도 `request` 톤(인디고)으로 통일. 사이드 상태 뱃지(`.status-badge`)에 `todo/changes/done` 변형 추가(기존엔 review 변형만 있어 나머지가 파란색으로 떨어지던 버그 수정). 카드 본문의 행위자·시각·승인노트는 5·6단계 데이터라 지시자 이름만 넣어 근사. ### 저장소 생성 폼 보강 ✅ (소유자/설명/템플릿 — 백엔드+프론트) **소유자 하드코딩 제거** - 백엔드 `GET /repos/meta` → `{ owner, template:{available,slug} }`(컨트롤러에서 `:repoId` 보다 먼저 선언). owner=`GITEA_ORG`(미연동 시 상수). - 프론트 `useRepo.meta()` + `repo.store`(`owner`/`templateAvailable`/`templateSlug`/`fetchCreateMeta`). **RepoCreatePage** 고정표시·**RepoListPage** 배너의 `marketing-team` 하드코딩 → 실제 조직값. **프로젝트 설명 필수화** - `CreateRepoDto.description` 필수(`@IsNotEmpty`, ≤300자). 프론트 생성폼 라벨 `*`·검증 추가. 누락 시 `VAL_001`(400). **템플릿 복제 생성** - `GiteaService.generateFromTemplate()` — Gitea generate API(`POST /repos/{tplOwner}/{tplRepo}/generate`, `git_content:true`)로 템플릿 저장소를 복제. 기본 브랜치는 템플릿을 따름. - 템플릿 출처 env `GITEA_TEMPLATE_OWNER`/`GITEA_TEMPLATE_REPO`(기본 `TtiPo/project-base`, Gitea 에서 *template* 설정 필요). - `CreateRepoDto.useTemplate=true` 면 빈 저장소 대신 복제 생성. 프론트 생성폼 템플릿 드롭다운=실제 옵션(없음 / `TtiPo/project-base`). - 라이브 검증: 생성된 repo 의 파일 트리가 `project-base` 와 동일하게 복제됨 확인. --- ## 3. 남은 단계 ### 5단계 — 업무 상세 부속 - 체크리스트 **개별 항목 토글/추가/삭제 영속**(현재 항목은 읽기 전용, 단 *승인 완료 시 일괄 완료*는 4단계에서 구현), 댓글/답글, **파일 첨부**, 승인 노트(승인 요청/수정 요청 시 메시지). - 승인 워크플로우 전용 엔드포인트(`approval/request|approve|changes`)는 상태 머신(4단계 `POST .../status`)에 노트·첨부를 더하는 형태로 확장. - **결정 필요**: 파일 저장소(로컬 볼륨 vs S3/MinIO). ### 6단계 — 활동 피드 - 2~5단계 행위를 이벤트로 적재(쓰기 API 없음, 읽기만). `GET /repos/:repoId/activity`. - 프론트: **RepoActivityPage**, TaskDetail 활동 탭. - **결정 필요**: 활동 표현 — 현재 mock은 HTML 통문자열 → 구조화(타입+행위자+대상) 권장. ### 7단계 — 내 업무 집계 - `GET /tasks/assigned`·`/tasks/issued`(저장소 횡단 집계). 프론트: **MyTasksPage**(담당/지시 탭). - **정리 대상**: 현재 mock `MyTasksPage` 가 유일하게 `…/tasks/:taskId/view`(`TaskAssigneePage`)로 링크 중. 4단계에서 상세(`TaskDetailPage`)가 역할 기반으로 통합됐으므로, 이 단계에서 MyTasks 를 실데이터로 연동하며 `/view` 링크를 `…/tasks/:taskId` 로 바꾸고 `TaskAssigneePage`/`task-assignee-view` 라우트를 제거한다. ### 8단계 — 마무리(선택) - 소셜 로그인(Google/Kakao), 이메일 인증, 알림, AI 에이전트 패널, Redis 캐싱, 전체 페이지네이션 통일. --- ## 4. 전환기 한계 (현재 시점) - **RepoActivityPage** 는 아직 mock `findRepo` 사용(6단계에서 해소). (RepoMembersPage 는 3단계, RepoDetail 업무 목록은 4단계에서 프론트 연동 완료) - **업무 상세의 댓글·체크리스트 토글·첨부·승인 노트**는 5단계 영역 — 현재 댓글/에이전트는 로컬 데모, 첨부/승인노트 UI 는 자리만 표시. 진행률은 4단계 집계로 실데이터. - `src/mock/relay.mock.ts` 는 아직 타입 정의(`Repo`/`User`/`TaskStatus`/`RepoTask`/`TaskDetail` 등) + 미연동 화면(활동/내업무) 데이터 공급원으로 사용 중. 해당 도메인이 연동되면 데이터는 제거하고 타입만 `src/types/` 로 이전. --- ## 5. 알아둘 사항 / 결정 기록 - **엔티티 순환참조**: 서로 참조하는 관계(예: `Repo`↔`RepoMember`)의 **단일 클래스 관계 속성에는 TypeORM `Relation<>` 래퍼**를 사용한다. 안 그러면 SWC 환경에서 `Cannot access 'X' before initialization` 크래시. → 4·5단계(Task↔Repo, Comment↔Task 등)에도 동일 적용. - **raw 집계 쿼리용 FK 컬럼명**: `createQueryBuilder().select('task.repo_id', …)` 처럼 **raw SQL 로 FK 컬럼을 직접 참조**하면, `@ManyToOne` 의 기본 FK 컬럼명이 camelCase(`repoId`)라 `task.repo_id`(snake) 와 불일치해 `42703 column does not exist` 가 난다. → 관계에 **`@JoinColumn({ name: 'repo_id' })` 를 명시**해 컬럼명을 snake_case 로 고정(프로젝트 컬럼 네이밍과도 일치). `Task.repo` 에 적용. (`task.repo = :id` 같은 관계 쿼리는 자동 매핑되어 무관) - **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 없이도 기존 흐름 유지. - **저장소 생성 폼 보강**: ① **프로젝트 설명 필수화**(`CreateRepoDto.description` required, 프론트 검증 추가). ② **템플릿 복제** — `useTemplate=true` 면 Gitea generate API 로 `GITEA_TEMPLATE_OWNER/REPO`(기본 `TtiPo/project-base`, *template* 설정 필요) 저장소를 복제 생성(브랜치는 템플릿을 따름). 프론트는 `GET /repos/meta`(owner+template) 로 owner 고정표시·템플릿 옵션을 받아 하드코딩 제거. - **import 저장소 멤버 데드락 / `REPO_IMPORT_ADMIN_EMAIL`**: import 저장소는 멤버 0명 → admin 부재 → 초대 불가의 데드락. env(`REPO_IMPORT_ADMIN_EMAIL`)로 지정한 가입 사용자를 동기화 시 멤버 0명인 Gitea-연동 저장소에 owner-admin 으로 자동 지정해 해소. (선택지 중 "env 지정 이메일" 채택 — 동기화는 시스템 작업이라 실행 주체가 없기 때문) - **`subRole`(저장소 내 직무) 제거**: 표시/권한 어디에도 관여하지 않는 장식 라벨이라 백엔드 엔티티·DTO·응답 + 프론트 타입·표시·초대입력에서 **완전 제거**(2026-06-17). owner 에 박던 `'리드'` 기본값도 제거. dev `synchronize` 가 `repo_members.sub_role` 컬럼을 드롭함. (권한은 `roleType`(admin/member)만 사용. 전사 직무 `User.role` 은 별개로 유지) - **env 변경 시 컨테이너 재생성 필요**: `docker-compose` 의 `env_file`(루트·`backend/.env.*`)은 **컨테이너 시작 시 1회만** 주입된다. `nest start --watch` 의 핫리로드는 **코드만** 다시 읽고 `process.env` 는 갱신하지 않으므로, env 값을 바꾸면 `up -d`(설정 변경 감지 시 재생성) 또는 `up -d --force-recreate <서비스>` 로 컨테이너를 다시 만들어야 한다. `docker compose restart` 는 env_file 을 다시 읽지 않음. (코드 변경만이면 watch 가 자동 반영) - **env 파일 구조 정규화**: 각 실제 `.env.{env}` 와 그 `.env.{env}.example` 은 **주석·키 순서·빈 줄까지 동일**하게 맞추고 차이는 값(실제 secret ↔ placeholder)만 두도록 정리. frontend 는 고유 키가 없어 헤더 주석만 동일하게 둠. - **전역 `relay.css` 모달 규칙 상속 주의**: 공통 `.modal-body`(text-align:center)·`.mbtn`(flex:1)·`.modal-backdrop` 가 전역이라, 다른 레이아웃의 커스텀 모달(예: `.invite-modal`)은 **스코프 클래스로 `text-align`/`flex` 를 명시적으로 리셋**해야 한다. (초대 모달 본문 좌측정렬·푸터 버튼 폭 깨짐의 원인이었음) --- ## 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`·`tasks`·`checklist_items`·`task_assignees` 테이블 자동 생성(Gitea 연동 컬럼 포함)·스키마 변경 자동 반영(예: `sub_role` 컬럼 드롭, `tasks.repo_id` FK 컬럼명). DB가 비어 있으므로 **회원가입 후 로그인**. 엔티티 신규 추가(4단계) 시 백엔드 컨테이너 재기동으로 스키마 반영. - 포트: 프론트 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 측 반영 확인. 5. (선택) `REPO_IMPORT_ADMIN_EMAIL` 에 **가입된** 사용자 이메일을 넣으면, 동기화 시 멤버 0명 import 저장소에 그 사용자가 owner-admin 으로 지정된다. **env 변경이므로 컨테이너 재생성 필요**(`up -d --force-recreate backend`). 부팅 로그의 `import 저장소 owner-admin 지정: ` 로 확인.