Skip to content

API 설계

truloop-core의 API 설계 원칙, 라우트 규칙, DTO 패턴, 인증 체계를 설명합니다.


라우트 체계

RouteRegistry.configureAllRoutes()에서 전체 API를 인증 수준과 버전별로 구성합니다.

API 경로 구조

경로인증 방식용도
/core/api/v1/*JWT (auth-jwt) + Public표준 API v1
/core/api/v3/*JWT (auth-jwt) + PublicAPI v3 (Loop, Recap(내부명: Story), Media)
/core/api/public/v1/*없음 (Public)외부 공개 API
/core/internal/*Bearer API Key (internal-api)서비스 간 Internal API
/core/admin/v1/*Google Workspace OIDC관리자 전용

v1 API 라우트

JWT 인증이 필요한 주요 엔드포인트:

/core/api/v1/
├── auth/                    # 인증 (토큰 갱신, 로그아웃 등)
├── users/                   # 사용자 관리
├── profiles/                # 프로필 관리
├── devices/                 # 디바이스 등록
├── blocks/                  # 차단
├── contacts/                # 연락처
├── loops/.../eta/           # ETA (예상 도착 시간)
├── content-templates/       # 콘텐츠 템플릿
├── cover-templates/         # 커버 템플릿 (GET 목록/상세, 카테고리 필터, 페이징)
├── short-forms/             # 숏폼 콘텐츠 (미사용/폐기 예정)
├── chats/                   # 채팅
├── notification-center/     # 알림 센터
├── live-activities/         # Live Activity
├── missions/                # 미션
└── secretary/               # AI 비서

Public 라우트 (인증 불필요):

/core/api/v1/
├── auth/                    # 회원가입, 로그인
├── phone-verification/      # 전화번호 인증
├── app-versions/            # 앱 버전 체크
└── users/                   # 사용자명 중복 확인 등

v3 API 라우트

Loop 중심의 리소스 계층 구조:

/core/api/v3/
├── loops/                        # 룹 CRUD, 카테고리, 추천 친구 등
│   ├── categories                # 룹 카테고리 목록
│   ├── recommended-friends       # 추천 친구
│   ├── seed-members              # 시드 멤버 후보
│   ├── recommended-groups        # 추천 그룹
│   ├── participants/             # 룹 참여 (join)
│   └── {eid}/
│       ├── participants/         # 참여자 관리
│       ├── invitations/          # 초대 관리
│       ├── media/                # 미디어 관리 + 태그된 사용자
│       │   └── groups/{group_eid}/
│       │       └── representative  # 대표 사진 지정/해제
│       ├── comments/             # 댓글
│       ├── cover-selection/      # 커버 선택
│       ├── reports/              # 룹 신고
│       ├── views                 # 룹 조회 기록
│       ├── availability          # 일정 조율 응답
│       ├── personalized-poster   # 개인화 포스터 생성
│       └── process               # 미디어 배치 처리
├── loops/{loop_eid}/stories/     # 리캡 관리 (내부명: Story)
│   └── {story_eid}/
│       ├── content               # 리캡 텍스트 편집
│       ├── regenerate            # 리캡 재생성
│       ├── like                  # 좋아요
│       └── share-link            # 공유 링크 생성
└── users/{eid}/reports/          # 사용자 신고
GET /core/api/v3/loops — 쿼리 파라미터 상세
파라미터타입설명
next_tokenstring커서 기반 페이지네이션 토큰
limitint페이지 크기
sortenumdatetime_asc, datetime_desc, created_at_asc, created_at_desc
filterenumall, created_by_me, invited_to
statusenum[]배열 쿼리 지원 (?status=collecting&status=confirmed). 값: all, waiting, collecting, collected, confirmed
viewenumhistory (기본값, 24시간 전 룹), upcoming (미래 + 조율 중), all
qstring검색어
PATCH .../stories/{story_eid}/content — 리캡 텍스트 편집

block_index 기반 partial patching으로 텍스트 블록만 수정합니다. 블록 구조(순서, 개수)는 보존됩니다.

Request:

json
{
  "text_edits": [
    { "block_index": 0, "text": "수정된 텍스트" }
  ]
}

Response: 수정된 content_blocks (JsonObject)

  • 룹 참여자만 수정 가능 (isParticipant 검증)
  • 중복 block_index가 있으면 InvalidTextEdit 에러
GET /core/api/v3/loops/{eid}/media — 그룹 메타데이터 포함 미디어 목록

GetGroupedLoopMediaUseCase를 통해 미디어 목록과 그룹 정보를 함께 반환합니다.

쿼리 파라미터:

파라미터타입기본값설명
limitint20페이지 크기 (1~100)
offsetint0오프셋

정렬: capturedAt 내림차순 (fallback: createdAt)

응답 최상위 필드: media (미디어 항목 배열)

응답 필드 (각 미디어 항목):

  • comments_count — 해당 미디어의 활성 댓글 수

  • tagged_users — 태그된 사용자 목록 (UserBaseDto)

  • group — 그룹 메타데이터 (미소속이면 null)

    • group_id: 그룹 식별자
    • is_representative: 대표 사진 여부
    • member_count: 그룹 내 미디어 수
    • member_eids: 그룹 내 미디어 EID 목록
  • 인증: JWT 필수, 룹 참여자만 조회 가능

PUT /core/api/v3/loops/{eid}/media/groups/{group_eid}/representative — 대표 사진 수동 설정

Request:

json
{
  "media_eid": "string"
}

Response: 204 No Content

검증:

  • 룹 존재 확인
  • 요청자가 룹 참여자인지 확인
  • 그룹이 캐시된 그룹핑 데이터에 존재하는지 확인
  • 지정한 미디어가 해당 그룹에 소속되어 있는지 확인

에러:

  • 404: 루프/그룹 없음 (LoopNotFound, GroupingNotFound, GroupNotFound)
  • 400: 미디어가 그룹에 미소속 (MediaNotInGroup)
  • 403: 비참여자 (NotLoopParticipant)
DELETE /core/api/v3/loops/{eid}/media/groups/{group_eid}/representative — 대표 사진 해제

대표 사진 오버라이드를 제거합니다. 이후 자동 선택(좋아요 수 최다)으로 복원됩니다.

Response: 204 No Content

검증: 룹 존재, 참여자 확인

POST /core/internal/grouping/process-queue — 이미지 그룹핑 배치 처리

Internal API (Bearer API Key 인증). EventBridge에 의해 주기적으로 호출됩니다.

Request:

json
{
  "loop_eids": ["string"]
}

Response:

json
{
  "processed": 3,
  "skipped": 1,
  "failed": 0
}
  • processed: 그룹핑이 성공적으로 수행된 루프 수
  • skipped: 조건 미달로 건너뛴 루프 수 (미디어 부족, 시간 분산 초과, 동시성 변경 등)
  • failed: 처리 중 에러가 발생한 루프 수
GET /core/admin/v1/users — 유저 목록 조회

Google Workspace OIDC 인증 (admin-google-oidc). 관리자 전용.

쿼리 파라미터:

파라미터타입설명
searchstringusername, name, phoneNumber, email ILIKE 검색
is_activeboolean활성 상태 필터
pageint페이지 번호 (1-based, 기본값: 1)
limitint페이지 크기

정렬: createdAt 내림차순

응답: 유저 목록 + 페이지네이션 메타 (offset 기반)

GET /core/admin/v1/users/{eid} — 유저 상세 조회

응답 필드: eid, name, username, email, phoneNumber, locale, birthDate, isActive, isProfilePrivate, profileImageUrl, createdAt, updatedAt 등

PATCH /core/admin/v1/users/{eid} — 유저 정보 수정

Partial update — null이 아닌 필드만 업데이트.

Request:

json
{
  "name": "string",
  "username": "string",
  "email": "string",
  "locale": "string",
  "birth_date": "string (yyyy-MM-dd)",
  "is_active": true,
  "is_profile_private": false
}

검증:

  • username: 도메인 유효성 검증 + 중복 체크
  • locale: Java Locale 유효성 검증
  • birthDate: 미래 날짜 불가

이벤트:

  • isActive=false → UserDeactivatedEvent 발행
  • isActive=true → UserReactivatedEvent 발행
  • 항상 UserUpdatedEvent 발행

Public API 라우트

인증 없이 접근 가능한 외부 공개 API:

/core/api/public/v1/
├── loops/                        # 초대 토큰 기반 룹 상세 조회
│   └── {loop_eid}/
│       ├── media                 # 룹 미디어
│       └── comments              # 룹 댓글
├── stories/                      # 공개 리캡 조회
│   ├── ?username=xxx             # 사용자별 공개 리캡 목록
│   └── {story_eid}/
│       └── comments              # 리캡 댓글
└── invitations/{token}/          # 초대 토큰 기반 비회원 참여
    ├── media                     # 미디어 조회
    ├── comments                  # 댓글 조회
    ├── guest                     # 비회원 참여 등록
    └── check-phone               # 전화번호 가입 여부 확인

Internal API

서비스 간 통신에 사용되며, Bearer API Key로 인증합니다:

/core/internal/
├── users/                        # 사용자 정보 조회
├── contacts/                     # 연락처 정보
├── guest-participants/           # 비회원 참여자 (내부명: GuestParticipant)
├── stories/                      # 리캡 (내부명: Story) - Worker 콜백 포함
│   └── {story_eid}/
│       └── requests/{request_id} # Worker 완료 콜백
├── grouping/
│   └── process-queue             # 이미지 그룹핑 배치 처리 (EventBridge 트리거)
└── loops/                        # 룹 CRUD + 참여자 관리
    └── {eid}/
        └── participants/         # 참여자 추가/제거/수정

Admin API 라우트

Google Workspace OIDC 인증이 필요하며, ADMIN_GOOGLE_CLIENT_ID 설정 시에만 활성화됩니다:

/core/admin/v1/
├── validate-token/               # 토큰 검증 (GET, 204 No Content)
├── dashboard/                    # 관리자 대시보드
│   ├── summary (GET)             # 주요 지표 요약 (총 사용자, DAU, 일별 룹, 회원 변동)
│   └── time-series (GET)         # 시계열 데이터 (DAU, 일별 룹 추이, ?days=30)
├── users/                        # 유저 관리
│   ├── (GET)                     # 유저 목록 조회 (search, isActive, page, limit)
│   └── {eid}/
│       ├── (GET)                 # 유저 상세 조회
│       └── (PATCH)               # 유저 정보 수정 (name, username, email, locale, birthDate, isActive, isProfilePrivate)
├── cover-templates/              # 커버 템플릿 CRUD
│   ├── upload-url/ (POST)        # 업로드 URL 발급
│   ├── (POST)                    # 생성
│   ├── (GET)                     # 목록 조회 (status, category, page, limit 필터)
│   └── {eid}/
│       ├── (GET)                 # 상세 조회
│       ├── (PATCH)               # 수정
│       ├── (DELETE)              # 삭제 (204 No Content)
│       └── status (PATCH)        # 상태 변경 (DRAFT → PUBLISHED → ARCHIVED, 재게시 가능)
└── sample-loops/                 # 샘플 룹 관리
    ├── resolve-invite/ (POST)    # 초대 토큰으로 룹 미리보기 조회
    ├── (POST)                    # 샘플 룹 등록
    ├── (GET)                     # 샘플 룹 목록 조회
    └── {eid}/
        ├── (PATCH)               # 샘플 룹 수정
        └── (DELETE)              # 샘플 룹 삭제

라우트 설계 원칙

Route → Controller → UseCase 패턴

주의

Route 파일(*Routes.kt)에서 UseCase.execute()를 직접 호출하는 것은 금지됩니다. Gradle 가드레일 태스크(verifyRouteUseCaseDelegation)가 이를 CI에서 자동으로 검증합니다.

책임 분리:

계층책임
RouteHTTP 관심사만 처리: 경로/쿼리/헤더/바디 파싱, 인증 컨텍스트, 상태 코드/응답
Controller/HandlerUse Case 호출 및 응답 매핑 오케스트레이션
UseCase순수 비즈니스 로직 실행

네이밍 규칙:

  • *Controller: 기능 단위 엔드포인트 그룹
  • *Handler: 좁은 범위의 특정 작업 처리

인증 경계 규칙

  • 인증 경계 (authenticate("auth-jwt"), authenticate("internal-api"))는 RouteRegistry에서만 설정
  • Feature 라우트 함수는 기본적으로 auth-neutral (인증 불가지론적)
  • Public/Authenticated 혼합 도메인: {feature}PublicRoutes() / {feature}AuthenticatedRoutes()로 분리

DTO 패턴

Request/Response DTO

  • DTO는 adapter:web 모듈에 위치
  • kotlinx-serialization@Serializable 사용
  • 도메인 모델과의 매핑은 Presentation Mapper에서 수행
kotlin
// Request DTO (adapter:web)
@Serializable
data class CreateLoopRequestDto(
    val datetime: String? = null,        // null이면 COLLECTING 상태로 생성
    val timezone: String? = null,
    val location: CreateLoopLocationDto? = null,
    val participants: List<String>,      // 참여자 user EID 목록
    val title: String? = null,
    val description: String? = null,
    @SerialName("cover_template_eid") val coverTemplateEid: String? = null,
    @SerialName("proposed_dates") val proposedDates: String? = null,
    val category: LoopCategoryDto? = null,
    @SerialName("availability_deadline") val availabilityDeadline: String? = null,  // ISO 8601 UTC
)

// Response DTO (adapter:web)
@Serializable
data class CreateLoopResponseDto(
    val eid: String,                     // 항상 EID (내부 ID 노출 금지)
    val title: String? = null,
    val datetime: String? = null,
    val status: LoopStatusDto,
    val timezone: String,
    val location: LoopLocationDto,
    val participants: List<LoopParticipantDto>,
    @SerialName("created_at") val createdAt: String,
)

정보

DTO 필드명은 snake_case(@SerialName 사용)로 직렬화합니다. kotlinx-serialization@SerialName 어노테이션으로 JSON 필드명을 지정합니다.

핵심 규칙

위험

  • 외부 노출 ID는 항상 EID (TSID 기반 문자열). 내부 DB ID는 절대 노출하지 않음
  • DTO ↔ Domain 매핑은 Presentation Mapper에서 수행. Domain 모델이 외부 의존성에 종속되지 않도록 함
  • 검증은 Presentation 계층에서 수행. Domain Entity는 이미 유효한 상태만 수신

인증 체계

JWT 인증

클라이언트 요청에 대한 사용자 인증:

Authorization: Bearer <JWT_TOKEN>
  • JWT 발급/검증은 adapter:external/jwt 패키지에서 처리
  • Security 플러그인 (configureSecurity)에서 Ktor authenticate("auth-jwt") 설정

Internal API 인증

서비스 간 통신을 위한 API Key 인증:

Authorization: Bearer <INTERNAL_API_KEY>
  • authenticate("internal-api") 블록으로 보호
  • truloop-ai-server, truloop-assistant 등 내부 서비스에서 사용

Admin 인증

관리자 페이지 접근을 위한 Google Workspace OIDC:

  • authenticate("admin-google-oidc") 블록으로 보호
  • ADMIN_GOOGLE_CLIENT_ID 환경변수가 설정된 경우에만 활성화
  • Google OIDC 클레임 검증 로직(validateGoogleOidcPayload)은 Swagger UI 접근 보호와 공유

응답 형식

성공 응답

json
{
    "eid": "ABC123XYZ",
    "title": "주말 나들이",
    "status": "CONFIRMED",
    "created_at": "2026-03-09T10:00:00Z"
}

에러 응답

ErrorDetail 구조체로 통일되어 있으며, StatusPagesConfig에서 모든 예외를 변환합니다:

json
{
    "type": "ERROR_TYPE_NOT_FOUND",
    "display_type": "DISPLAY_TYPE_SNACKBAR",
    "message": {
        "locale": "ko-KR",
        "text": "Loop을 찾을 수 없습니다"
    },
    "metadata_proto_json": null
}
필드설명
type머신 리더블 에러 코드 (i18n 클라이언트 번역용)
display_type클라이언트 UI 힌트 (DISPLAY_TYPE_NONE, DISPLAY_TYPE_SNACKBAR, DISPLAY_TYPE_DIALOG, DISPLAY_TYPE_UNSPECIFIED)
message로캘 정보(locale)와 번역된 텍스트(text)를 포함하는 객체
metadata_proto_json추가 컨텍스트 정보 (JSON 문자열, nullable)

예외 → HTTP 상태 코드 매핑

BusinessException sealed class 기반:

예외 클래스HTTP 상태 코드
NotFoundException404 Not Found
ForbiddenException403 Forbidden
BadRequestException400 Bad Request
ConflictException409 Conflict
UnauthorizedException401 Unauthorized
ServiceUnavailableException503 Service Unavailable
InternalException500 Internal Server Error

InfrastructureException(외부 서비스, DB, 네트워크 장애)은 500/502/503으로 변환되며, Sentry로 자동 보고됩니다.


OpenAPI 문서 자동 생성

io.github.smiley4:ktor-openapi + ktor-swagger-ui 플러그인으로 모든 API 엔드포인트의 OpenAPI 스펙을 자동 생성합니다.

항목
Swagger UIGET {basePath}/swagger-ui
OpenAPI JSONGET {basePath}/swagger-ui/api-docs.json
플러그인io.github.smiley4:ktor-openapi, io.github.smiley4:ktor-swagger-ui, io.github.smiley4:schema-kenerator-core

Swagger UI 접근 보호

ADMIN_GOOGLE_CLIENT_ID 환경변수가 설정된 경우, Swagger UI는 Google OAuth 쿠키 인증으로 보호됩니다. 미설정 시 기존과 동일하게 공개 접근됩니다 (로컬 개발 호환).

라우트 구조 (보호 활성화 시):

MethodPath설명
GET{basePath}/swagger-loginGoogle Sign-In 로그인 페이지
POST{basePath}/swagger-auth/callbackID 토큰 검증 후 쿠키 설정
GET{basePath}/swagger-ui/**쿠키 인증 게이트 (미인증 시 /swagger-login으로 리다이렉트)

인증 흐름:

아키텍처:

  • 구현 위치: bootstrap/plugins/SwaggerAuth.kt
  • SwaggerTokenValidator — 쿠키에서 ID 토큰을 추출하고 JWKS로 RSA256 서명을 검증한 뒤, validateGoogleOidcPayload()로 클레임을 검증
  • SwaggerCookieAuth — Ktor createRouteScopedPlugin으로 구현된 라우트 스코프 플러그인. /swagger-ui 라우트에만 설치
  • Admin 인증과 동일한 validateGoogleOidcPayload() 함수를 공유하여 Google OIDC 검증 로직 중복을 방지
  • ktor-server-html-builder 의존성으로 로그인 페이지를 서버사이드 렌더링

정보

configureSwaggerRoutes()adminGoogleOidcConfig가 null이면 기존과 동일하게 인증 없이 Swagger UI를 제공합니다. 로컬 개발 환경에서는 ADMIN_GOOGLE_CLIENT_ID를 설정하지 않으면 됩니다.

DTO 스키마 설명

schema-kenerator-core 라이브러리의 @Description 어노테이션으로 DTO 프로퍼티에 설명을 추가하면 OpenAPI JSON 스키마의 description 필드로 자동 반영됩니다. 한국어 설명을 사용합니다.

kotlin
@Serializable
data class LoopDetailResponseDto(
    @Description("루프 고유 식별자")
    val eid: String,
    @Description("루프 제목")
    val title: String?,
    @Description("일정 날짜/시간 (COLLECTING 상태에서는 null)")
    val datetime: String?,
    @Description("루프 상태")
    val status: LoopStatusDto,
    // ...
)

Route 핸들러에서 response { code(HttpStatusCode.OK) { body<ResponseDto>() } } 패턴으로 응답 DTO 타입을 명시하면 Swagger UI에서 응답 스키마를 확인할 수 있습니다.

kotlin
op.response {
    code(HttpStatusCode.OK) {
        body<LoopDetailResponseDto>()
        description = "루프 상세 조회 성공"
    }
}

인증 스키마

OpenAPI 스펙에 정의된 보안 스키마:

스키마 이름타입설명
auth-jwtHTTP Bearer (JWT)클라이언트 JWT 인증 (기본 보안 스키마)
internal-apiHTTP Bearer내부 서비스 간 API 키 인증
admin-google-oidcOpenID ConnectGoogle Workspace OIDC 관리자 인증

라우트 태그 규칙

각 라우트 파일의 엔드포인트는 tags() 함수로 API 그룹을 지정합니다. 태그명은 경로 prefix 기반입니다 (예: api/loops, api/stories, internal/loops).


변경 이력

날짜내용
2026-03-16Admin sample-loops 엔드포인트 5개 추가 (resolve-invite, CRUD)
2026-03-16Grouped Media API 응답 스키마 변경 (items→media, commentsCount/taggedUsers 추가), Admin 유저 관리 API 추가
2026-03-12DTO 스키마 설명(@Description) 및 응답 타입 명시(body<ResponseDto>()) 패턴 추가
2026-03-12Swagger UI Google OAuth 쿠키 인증 보호 추가 (ADMIN_GOOGLE_CLIENT_ID 설정 시 활성화)
2026-03-12이미지 그룹핑 API 추가 — v3 미디어 그룹 라우트, 대표 사진 CRUD, Internal 배치 처리
2026-03-12OpenAPI 문서 자동 생성 섹션 추가 (Swagger UI, 인증 스키마, 태그 규칙)
2026-03-12CreateLoopRequestDto에 availability_deadline 필드 반영
2026-03-11availabilityDeadline 검증 에러 추가: ERROR_TYPE_DEADLINE_IN_PAST (400), ERROR_TYPE_DEADLINE_AFTER_PROPOSED_DATES (400)
2026-03-11Admin API dashboard 엔드포인트 추가 (summary, time-series)
2026-03-10Admin API cover-templates 하위 구조 상세화 (CRUD + upload-url + status 변경), 리캡 텍스트 편집 API 상세 추가, 룹 리스트 API 쿼리 파라미터 상세 추가, v1 커버 템플릿 설명 보강
2026-03-10소스 코드 기준으로 전면 검증: v3 라우트 상세화(stories 경로 수정, 서브 리소스 추가), Public API/Admin API 라우트 추가, 에러 응답 형식을 실제 ErrorDetail 구조체로 수정, DTO 예시를 실제 코드 기반으로 교체, 예외-HTTP 매핑 테이블 추가