Files
comrelay/docs/api-contract.md
T
ttipo 6027856cbb feat: 저장소 생성 시 Gitea 조직 연동 추가
외부 Gitea 인스턴스의 조직에 저장소를 자동 동기화한다.
- GiteaService: /api/v1 클라이언트(내장 fetch, 새 의존성 없음), enabled 게이트
- Repo 엔티티에 연동 컬럼(giteaRepoId/giteaFullName/cloneUrl/htmlUrl) 추가
- RepoService 생성/수정/삭제에 Gitea 동기화 와이어링(생성 우선·실패 시 롤백)
- GITEA_* env 문서화(.env.example), api-contract/진행현황 문서 갱신

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:17:33 +09:00

12 KiB

Relay API 계약서 (SSOT)

프론트(frontend/src/mock/relay.mock.ts) 인터페이스를 기준으로 도출한 백엔드 ↔ 프론트 공통 계약. 백엔드 엔티티/DTO와 프론트 타입은 이 문서를 단일 진실 공급원으로 삼아 양쪽이 동일하게 구현한다. 모든 응답은 루트 CLAUDE.md 의 표준 응답 래퍼({ success, data } / { success, error })로 감싸지며, 아래 표의 "응답"은 언래핑된 data 본문만 기술한다.

0. 공통 규약

인증 / 토큰

  • 이메일 + JWT만 1단계 구현. 소셜 로그인(Google/Kakao)·이메일 인증은 8단계로 보류.
  • Access / Refresh 토큰 모두 HttpOnly·Secure 쿠키로 전달 (웹 localStorage 금지).
    • Access: 짧은 만료(15분), 쿠키명 access_token
    • Refresh: 긴 만료(14일), 쿠키명 refresh_token, 경로 /api/auth
  • 프론트 axios 는 withCredentials: true (이미 적용됨) — 별도 Authorization 헤더 불필요.
  • 보호 라우트는 JwtAuthGuard. 미인증 시 401 → AUTH_001.

ID 전략

대상 타입 비고
User uuid 외부 노출 식별자
Repo uuid + slug URL 은 owner/name slug, 라우팅 segment 는 slug 마지막 토큰
Task number 저장소 내 순번(GitHub 이슈처럼 #9). 전역 PK 는 별도 uuid
그 외 uuid Comment, Attachment 등

파생 필드(저장하지 않고 계산)

다음은 백엔드가 원시값(타임스탬프/카운트)만 내려주고 프론트 shared/utils/format.ts 에서 계산한다.

  • updatedAgo("2시간 전 업데이트"), dueLabel("D-3", "2일 지남", "6월 9일 완료"), dday, ddayLevel
  • progressPct, progressLevel, progressLabel, doneCount/totalCount(서버 카운트 응답은 허용)
  • 아바타 initial/color — 서버는 name 만, color 는 userId 해시로 프론트 산출(또는 서버 저장)

따라서 API 응답의 날짜 필드는 모두 ISO 8601 문자열(dueDate, updatedAt, createdAt 등)로 내린다.

비즈니스 에러 (사용자 노출 문구)

도메인 검증 실패처럼 사용자에게 구체 문구를 보여줘야 하는 경우 BusinessException(code, message, status) (backend/src/common/exceptions/business.exception.ts)를 던진다. 전역 필터가 { code, message } 를 status 매핑보다 우선해 그대로 표준 응답으로 내려보내고, 프론트는 BIZ_001 코드일 때 서버 message 를 그대로 노출한다. 예: 로그인 실패 → BusinessException('BIZ_001', '이메일 또는 비밀번호가 올바르지 않습니다.', 401).

페이지네이션

목록 API 는 오프셋 기반: 쿼리 ?page=1&size=20, 응답 { items: [...], total, page, size }. (1단계에서는 단순 배열 허용, 7단계에서 페이지네이션으로 통일)


1. 인증 (/api/auth) — 1단계

메서드 경로 요청 응답 비고
POST /auth/signup { email, password, name } { user } 가입 후 자동 로그인(쿠키 set)
POST /auth/login { email, password, keepLogin? } { user } access/refresh 쿠키 set
POST /auth/logout null 쿠키 clear
POST /auth/refresh — (refresh 쿠키) null access 쿠키 재발급
GET /auth/me User 부트스트랩/가드용. 미인증 401

User 응답 형태:

{
  "id": "uuid",
  "email": "sykim@acme.co",
  "name": "김서연",
  "role": "콘텐츠 기획"   // 전사 직무(선택)
}

프론트 매핑: auth.store.ts(신규) + useAuth composable. 라우터 가드 TODO 를 me 부트스트랩 + 401→/login 리다이렉트로 교체. LoginPage/SignupPagerouter.push 를 실제 호출로.


2. 저장소 (/api/repos) — 2단계

메서드 경로 요청 응답
GET /repos ?page&size Repo[](요약) — 내가 멤버인 저장소
POST /repos CreateRepoDto Repo
GET /repos/:repoId Repo(상세)
PATCH /repos/:repoId UpdateRepoDto Repo
DELETE /repos/:repoId null

CreateRepoDto: { slug, name, visibility('private'|'public'), description?, branch? } Repo 응답(원시값 중심):

{
  "id": "q3-launch-campaign",      // slug 마지막 segment = 라우팅 id
  "name": "3분기 신제품 런칭 캠페인",
  "owner": "marketing-team",       // Gitea 연동 시 Gitea 조직명
  "slug": "marketing-team/q3-launch-campaign",  // = Gitea full_name
  "desc": "...",
  "visibility": "private",
  "branch": "main",
  "members": [ /* User 요약 배열, 최대 N명 + memberCount */ ],
  "memberCount": 5,
  "doneCount": 8,
  "totalCount": 12,
  "cloneUrl": "https://gitea.example.com/marketing-team/q3-launch-campaign.git",  // 미연동 시 null
  "htmlUrl": "https://gitea.example.com/marketing-team/q3-launch-campaign",       // 미연동 시 null
  "updatedAt": "2026-06-16T11:00:00Z"
}

moreCount, progressPct/Level/Label, updatedAgo 는 프론트 계산. 매핑 페이지: RepoListPage, RepoCreatePage, RepoDetailPage(헤더), RepoSettingsPage.

Gitea 연동 (외부 인스턴스)

저장소 생명주기를 외부 Gitea 조직에 동기화한다. GITEA_BASE_URL/GITEA_TOKEN/GITEA_ORG (backend .env) 가 모두 설정되면 활성화되고, 비어 있으면 Gitea 호출 없이 기존대로 동작한다.

Relay 동작 Gitea API 매핑
POST /repos POST /api/v1/orgs/{org}/repos (auto_init) slug→repo name, visibility→private, branch→default_branch, name+desc→description
PATCH /repos/:id PATCH /api/v1/repos/{org}/{slug} name/desc 변경→description, visibility 변경→private (slug 불변)
DELETE /repos/:id DELETE /api/v1/repos/{org}/{slug}
  • 인증: 조직 권한 토큰 단일 사용(Authorization: token <TOKEN>). 사용자별 Gitea 계정 매핑은 추후.
  • 생성은 Gitea 우선 → 성공 시 Relay 저장(연동 정보 giteaRepoId/cloneUrl/htmlUrl 보관). Relay 저장 실패 시 Gitea repo 롤백 삭제.
  • Gitea 이름 충돌(409) → BIZ_001 "이미 사용 중인 저장소 이름입니다.", 그 외 실패 → BIZ_001(502).
  • 수정/삭제의 Gitea 동기화는 베스트 에포트(실패해도 Relay 상태는 반영, 에러 로그만 남김).

3. 멤버 (/api/repos/:repoId/members) — 3단계

메서드 경로 요청 응답 권한
GET .../members RepoMember[] member+
POST .../members { email, roleType, subRole? } RepoMember admin
PATCH .../members/:userId { roleType?, subRole? } RepoMember admin
DELETE .../members/:userId null admin (owner 제거 불가)

RepoMember: { user: User, email, subRole, taskCount, roleType('admin'|'member'), owner } (isYou 는 프론트가 현재 사용자와 비교해 산출)

매핑 페이지: RepoMembersPage(초대 모달, 역할 변경, 제거).


4. 업무 (/api/repos/:repoId/tasks) — 4단계

메서드 경로 요청 응답
GET .../tasks ?status&assignee RepoTask[]
POST .../tasks CreateTaskDto TaskDetail
GET .../tasks/:taskId TaskDetail
PATCH .../tasks/:taskId UpdateTaskDto TaskDetail
DELETE .../tasks/:taskId null
POST .../tasks/:taskId/status { status } TaskDetail

상태(TaskStatus): todo | prog | review | done | changes. 전이 규칙(상태 머신):

todo ─시작→ prog ─승인요청→ review ─승인→ done
                 ▲                  └─수정요청→ changes ─재요청→ review
                 └──────────────────────────────────┘
  • POST .../status 는 일반 상태 변경.
  • 승인 워크플로우(5단계와 함께): 담당자 "승인 요청"(prog→review), 지시자 "승인"(review→done) / "수정 요청"(review→changes).

CreateTaskDto: { title, content?: string[], assigneeIds: uuid[], dueDate?: ISO, checklist?: {text}[] } RepoTask(목록 행): { id, title, status, dueDate, checklist: [done,total], commentCount, assignees: User[] } TaskDetail: mock 의 TaskDetail 인터페이스와 동일 — 단 라벨류는 원시값으로: { id, repoId, repoName, title, status, content[], checklist[], comments[], activities[], assignees[], issuer, dueDate, files[], approval?, createdAt }

매핑 페이지: RepoDetailPage(목록), TaskCreatePage, TaskDetailPage(지시자), TaskAssigneePage(담당자, state 분기).


5. 업무 상세 부속 — 5단계

체크리스트

메서드 경로 요청 응답
POST .../tasks/:taskId/checklist { text } ChecklistItem
PATCH .../checklist/:itemId { text?, done? } ChecklistItem
DELETE .../checklist/:itemId null

댓글 / 답글

메서드 경로 요청 응답
POST .../tasks/:taskId/comments { text, fileId? } Comment
POST .../comments/:commentId/replies { text } Reply

파일 첨부

메서드 경로 요청 응답
POST .../tasks/:taskId/files multipart/form-data Attachment
DELETE .../files/:fileId null

결정 필요: 파일 저장소(로컬 볼륨 vs S3/MinIO). Attachment: { id, name, size, kind('pdf'|'zip'|'doc'|'img'), url }.

승인 워크플로우

메서드 경로 요청 응답
POST .../tasks/:taskId/approval/request { note } TaskDetail
POST .../tasks/:taskId/approval/approve TaskDetail
POST .../tasks/:taskId/approval/changes { note } TaskDetail

6. 활동 피드 — 6단계

활동은 2~5단계 행위에서 이벤트로 자동 적재(쓰기 API 없음, 읽기만).

메서드 경로 응답
GET /repos/:repoId/activity RepoActivityDay[](날짜 그룹)
(업무 활동) TaskDetail.activities 에 포함 Activity[]

RepoActivityItem: { tone, icon, actor: User, payload, createdAt }

서버는 이벤트 타입 + 행위자 + 대상만 저장하고, 표시 HTML 문구는 프론트에서 조립(현재 mock 은 html 통문자열 — 연동 시 구조화 권장). 매핑: RepoActivityPage, TaskDetail 활동 탭.


7. 내 업무 (/api/tasks) — 7단계

메서드 경로 요청 응답
GET /tasks/assigned MyTaskGroup[](상태 그룹) — 내가 담당
GET /tasks/issued MyTaskGroup[] — 내가 지시

저장소 횡단 집계. MyTaskGroup/MyTaskRow 는 mock 인터페이스와 동일하되 라벨류 제외, 원시값(dueDate, status) 기반.

매핑 페이지: MyTasksPage(담당/지시 탭, 상태 그룹). countActive 는 프론트 유지.


8. 마무리(선택) — 8단계

소셜 로그인(Google/Kakao OAuth), 이메일 인증, 알림, AI 에이전트 패널, Redis 캐싱, 전체 페이지네이션 통일.


백엔드 모듈 매핑 (구현 순서)

src/modules/
  auth/      (1) — User 엔티티, JWT 전략, Guard, 쿠키
  repo/      (2) — Repo 엔티티
  member/    (3) — RepoMember 조인 엔티티 (repo 모듈에 흡수 가능)
  task/      (4,5) — Task / ChecklistItem / Comment / Reply / Attachment
  activity/  (6) — Activity 이벤트 (다른 모듈에서 emit)
  my-tasks/  (7) — 집계 전용 (task 모듈에 흡수 가능)