Skip to content

Latest commit

 

History

History
1888 lines (1591 loc) · 50.8 KB

File metadata and controls

1888 lines (1591 loc) · 50.8 KB

Feed API 명세서

개요

일상적인 피드 게시물 생성, 조회, 수정, 삭제 및 상호작용(좋아요, 북마크, 댓글) API를 제공합니다. 모든 API는 로그인이 필요하며, JWT 토큰 인증을 사용합니다.

기본 정보

  • Base URL: http://localhost:3001
  • 인증 방식: JWT Bearer Token (Authorization 헤더) 또는 HTTP-only 쿠키
  • 응답 형식: JSON

공통 응답 형식

성공 응답

{
  "code": 200,
  "message": "요청 성공 메시지",
  "data": {
    // 응답 데이터
  }
}

오류 응답

{
  "code": 400,
  "message": "오류 메시지",
  "errors": [
    {
      "field": "필드명",
      "message": "상세 오류 메시지"
    }
  ]
}

피드 API

1. 피드 이미지 업로드

POST /feeds/upload/images

피드에 사용할 이미지들을 업로드합니다.

Request

  • Content-Type: multipart/form-data
  • Body: images 필드에 이미지 파일들 (최대 10개)

Response Example

{
  "code": 200,
  "message": "이미지가 업로드되었습니다.",
  "data": {
    "files": [
      {
        "filename": "image_1640000000000_abc123.jpg",
        "path": "/uploads-feed/2024/01/15/image_1640000000000_abc123.jpg",
        "url": "http://localhost:3001/uploads-feed/2024/01/15/image_1640000000000_abc123.jpg",
        "size": 1024000
      },
      {
        "filename": "image_1640000000001_def456.png",
        "path": "/uploads-feed/2024/01/15/image_1640000000001_def456.png",
        "url": "http://localhost:3001/uploads-feed/2024/01/15/image_1640000000001_def456.png",
        "size": 2048000
      }
    ]
  }
}

파일 제한사항

  • 지원 형식: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif
  • 최대 파일 크기: 10MB
  • 최대 업로드 개수: 10개

2. 피드 영상 업로드

POST /feeds/upload/video

피드에 사용할 영상을 업로드하고 자동으로 썸네일을 생성합니다.

Request

  • Content-Type: multipart/form-data
  • Body: video 필드에 영상 파일 1개

Response Example

{
  "code": 200,
  "message": "영상과 썸네일이 업로드되었습니다.",
  "data": {
    "video": {
      "filename": "video_1640000000000_abc123.mp4",
      "path": "/uploads-feed-video/2024/01/15/video_1640000000000_abc123.mp4",
      "url": "http://localhost:3001/uploads-feed-video/2024/01/15/video_1640000000000_abc123.mp4",
      "size": 52428800
    },
    "thumbnail": {
      "filename": "thumbnail_video_1640000000000_abc123.jpg",
      "path": "/uploads-feed-video-thumbnail/2024/01/15/thumbnail_video_1640000000000_abc123.jpg",
      "url": "http://localhost:3001/uploads-feed-video-thumbnail/2024/01/15/thumbnail_video_1640000000000_abc123.jpg",
      "size": 245760
    }
  }
}

썸네일 생성 실패 시 응답:

{
  "code": 200,
  "message": "영상이 업로드되었습니다. (썸네일 생성 실패)",
  "data": {
    "video": {
      "filename": "video_1640000000000_abc123.mp4",
      "path": "/uploads-feed-video/2024/01/15/video_1640000000000_abc123.mp4",
      "url": "http://localhost:3001/uploads-feed-video/2024/01/15/video_1640000000000_abc123.mp4",
      "size": 52428800
    },
    "thumbnail": null
  }
}

파일 제한사항

  • 지원 형식: .mp4, .avi, .mov, .wmv, .flv
  • 최대 파일 크기: 100MB
  • 업로드 개수: 1개만
  • 썸네일: 영상의 50% 지점에서 자동 생성 (생성 실패 시 null 반환)

3. 피드 통합 파일 업로드 및 생성

POST /feeds/create-with-files

피드에 사용할 이미지/영상 파일들과 콘텐츠 블록을 함께 전송하여 피드를 생성합니다. 파일들은 자동으로 업로드되고 경로가 생성됩니다.

Headers

헤더명 타입 필수 설명
Idempotency-Key String 필수 중복 요청 방지용 UUID v4

Request

  • Content-Type: multipart/form-data
  • Body:
    • content_blocks 필드: JSON 문자열 형태의 콘텐츠 블록 배열
    • images 필드: 이미지 파일들 (최대 10개)
    • videos 필드: 영상 파일들 (최대 5개)

content_blocks JSON 구조 예시

[
  {
    "type": "text",
    "value": "피드 텍스트 내용입니다.",
    "sequence": 0
  },
  {
    "type": "image",
    "value": null,
    "sequence": 1
  },
  {
    "type": "video",
    "value": null,
    "editInfo": {
      "trimStart": 1000,
      "trimEnd": 5000,
      "cropArea": {
        "x": 0.1,
        "y": 0.1,
        "width": 0.8,
        "height": 0.6
      }
    },
    "sequence": 2
  }
]

Request Example (FormData)

const formData = new FormData();

// 콘텐츠 블록 JSON
const contentBlocks = [
  {
    type: 'text',
    value: '새로운 피드를 작성합니다!',
    sequence: 0
  },
  {
    type: 'image',
    value: null,  // 파일이 업로드될 예정
    sequence: 1
  },
  {
    type: 'video',
    value: null,  // 파일이 업로드될 예정
    sequence: 2
  }
];

formData.append('content_blocks', JSON.stringify(contentBlocks));

// 파일들 추가
formData.append('images', imageFile1);
formData.append('images', imageFile2);
formData.append('videos', videoFile1);

fetch('/feeds/create-with-files', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`
  },
  body: formData
});

Response Example

{
  "code": 201,
  "message": "피드가 작성되었습니다.",
  "data": {
    "feedId": 1,
    "created_at": "2024-01-15T10:30:00.000Z"
  }
}

파일 처리 규칙

  • 이미지 파일: images 필드의 파일들이 content_blocks에서 value: null인 이미지 타입 블록에 순서대로 매핑됩니다.
  • 영상 파일: videos 필드의 파일들이 content_blocks에서 value: null인 비디오 타입 블록에 순서대로 매핑됩니다.
  • 편집 정보: 비디오 블록에 editInfo가 포함된 경우 자동으로 trim + crop 처리가 수행됩니다.
  • 스토리지: 환경변수 STORAGE_DRIVER에 따라 로컬 또는 네이버 클라우드에 파일이 저장됩니다.

4. 피드 수정 (통합 파일 업로드) - 신규

PUT /feeds/{feedId}/update-with-files

피드의 텍스트, 이미지, 비디오를 한 번에 수정합니다. 기존 파일들은 자동으로 정리됩니다.

Request

  • Method: PUT
  • URL: /feeds/{feedId}/update-with-files
  • Content-Type: multipart/form-data
  • 인증: JWT Bearer Token 필수 (Authorization 헤더)
  • Path Parameters:
    • feedId (number, 필수): 수정할 피드 ID

FormData Fields

필드 타입 필수 설명
content_blocks string 필수 콘텐츠 블록 배열의 JSON 문자열
images[] File[] 선택 새 이미지 파일들 (다중 가능, 필드명: images)
videos[] File[] 선택 새 비디오 파일들 (다중 가능, 필드명: videos)

Content Blocks JSON 구조

[
  {
    "type": "text",
    "value": "수정된 텍스트 블록 내용",
    "sequence": 0
  },
  {
    "type": "image",
    "value": "/uploads-feed/image/2025/01/18/image_123.jpg", // 기존 파일 URL (유지)
    "sequence": 1
  },
  {
    "type": "image",
    "value": null, // 새 파일 (업로드됨)
    "sequence": 2
  },
  {
    "type": "video",
    "value": null, // 새 파일
    "editInfo": {
      "trimStart": 1000,
      "trimEnd": 5000,
      "cropArea": {
        "x": 0.1,
        "y": 0.1,
        "width": 0.8,
        "height": 0.8
      }
    },
    "sequence": 3
  }
]

Video 편집 정보 (editInfo)

비디오 블록에 editInfo를 포함하면 자동으로 trim + crop 편집이 적용됩니다:

필드 타입 필수 설명 단위
trimStart number 필수 편집 시작 시간 밀리초 (ms)
trimEnd number 필수 편집 종료 시간 밀리초 (ms)
cropArea.x number 필수 크롭 시작 X좌표 비율 (0.0 ~ 1.0)
cropArea.y number 필수 크롭 시작 Y좌표 비율 (0.0 ~ 1.0)
cropArea.width number 필수 크롭 너비 비율 (0.0 ~ 1.0)
cropArea.height number 필수 크롭 높이 비율 (0.0 ~ 1.0)

파일 업로드 트리거 조건

새 파일 업로드를 트리거하는 조건들:

조건 설명 예시
value: null 표준 방식 - 새 파일 업로드 "value": null
value.startsWith('file://') 로컬 파일 URI - 호환성 지원 "value": "file:///Users/.../image.jpg"
그 외 기존 파일 유지 "value": "/uploads/image123.jpg"

클라이언트 구현 가이드

📱 React Native / Expo:

const updateFeedWithFiles = async (feedId, updateData) => {
  const formData = new FormData();

  // content_blocks JSON
  const contentBlocks = [
    {
      type: 'text',
      value: '수정된 피드 텍스트',
      sequence: 0
    },
    {
      type: 'image',
      value: '/uploads-feed/image/2025/01/18/image_123.jpg', // 기존 이미지 유지
      sequence: 1
    },
    {
      type: 'image',
      value: null, // 새 이미지 업로드
      sequence: 2
    },
    {
      type: 'video',
      value: null, // 새 비디오 업로드
      editInfo: {
        trimStart: 1000,
        trimEnd: 5000,
        cropArea: { x: 0.1, y: 0.1, width: 0.8, height: 0.8 }
      },
      sequence: 3
    }
  ];

  formData.append('content_blocks', JSON.stringify(contentBlocks));

  // 새 파일들 추가
  updateData.newImages.forEach((image, index) => {
    formData.append('images', {
      uri: image.uri,
      type: image.type || 'image/jpeg',
      name: image.name || `image_${index}.jpg`,
    } as any);
  });

  updateData.newVideos.forEach((video, index) => {
    formData.append('videos', {
      uri: video.uri,
      type: video.type || 'video/mp4',
      name: video.name || `video_${index}.mp4`,
    } as any);
  });

  const response = await fetch(`/feeds/${feedId}/update-with-files`, {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${token}`,
      // Content-Type은 자동으로 설정됨
    },
    body: formData
  });

  return response.json();
};

// 사용 예시
const result = await updateFeedWithFiles(feedId, {
  newImages: [imageFile],
  newVideos: [videoFile]
});

🌐 웹 (React):

const updateFeedWithFiles = async (feedId, formData) => {
  const response = await fetch(`/feeds/${feedId}/update-with-files`, {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${token}`,
      // Content-Type은 자동으로 설정됨
    },
    body: formData,
  });

  return response.json();
};

// 사용 예시
const handleUpdate = async () => {
  const formData = new FormData();

  // content_blocks JSON
  const contentBlocks = [
    { type: 'text', value: '수정된 텍스트', sequence: 0 },
    { type: 'image', value: '/existing/image.jpg', sequence: 1 }, // 기존 유지
    { type: 'image', value: null, sequence: 2 }, // 새 파일
  ];

  formData.append('content_blocks', JSON.stringify(contentBlocks));

  // 새 파일들
  selectedImages.forEach(file => formData.append('images', file));
  selectedVideos.forEach(file => formData.append('videos', file));

  const result = await updateFeedWithFiles(feedId, formData);
};

처리 플로우

  1. 기존 콘텐츠 블록 조회 → 현재 파일들 파악
  2. 새 데이터 파싱 → FormData에서 텍스트/파일 분리
  3. 콘텐츠 블록 재구성 → 새 파일 업로드 + 기존 파일 유지
  4. 파일 비교 → 사용하지 않는 기존 파일들 식별
  5. DB 업데이트 → 트랜잭션으로 안전하게 저장
  6. 파일 정리 → 사용하지 않는 파일들 자동 삭제

특징

  • 자동 파일 정리: 기존 파일들 중 사용하지 않는 것들은 자동 삭제
  • 트랜잭션 보장: DB 업데이트 실패 시 파일 정리 취소
  • 비디오 편집 지원: 수정 시에도 trim + crop 적용 가능
  • URL 기반 비교: sequence와 무관하게 파일 존재 여부로 정리
  • 경쟁 상태 해결: 게시물 API와 동일한 안전한 파일 매핑 로직

파일 제한사항

  • 이미지: 최대 10개, 각 10MB, 지원 형식: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif
  • 영상: 최대 5개, 각 100MB, 지원 형식: .mp4, .avi, .mov, .wmv, .flv
  • 썸네일: 영상 파일에 대해 자동 생성

Response Example

{
  "code": 200,
  "message": "피드가 수정되었습니다.",
  "data": {
    "feedId": 123
  }
}

Error Handling

  • 권한 없음: 본인이 작성한 피드만 수정 가능
  • 피드 없음: 존재하지 않는 피드 ID
  • 파일 정리 실패: DB는 업데이트되지만 파일 삭제 실패 시 로그 기록
  • 트랜잭션 롤백: 업데이트 중 오류 발생 시 모든 변경사항 취소

기존 파일 정리 로직 상세

// 1. 기존 파일들 수집
const existingFileUrls = new Set<string>();
existingBlocks.forEach(block => {
  if ((block.type === 'image' || block.type === 'video') && block.value) {
    existingFileUrls.add(block.value);
  }
  if (block.type === 'video' && block.thumbnail_url) {
    existingFileUrls.add(block.thumbnail_url);
  }
});

// 2. 새 파일들 수집
const newFileUrls = new Set<string>();
processedBlocks.forEach(block => {
  if (block.type === 'image' || block.type === 'video') {
    newFileUrls.add(block.value);
  }
  if (block.type === 'video' && block.thumbnail_url) {
    newFileUrls.add(block.thumbnail_url);
  }
});

// 3. 삭제할 파일들 계산
const filesToDelete = Array.from(existingFileUrls).filter(
  url => !newFileUrls.has(url)
);

주의사항

  • 파일 URL 매칭: 정확한 파일 경로 비교로 불필요한 삭제 방지
  • 트랜잭션 안전성: DB 변경 후 파일 정리 실패 시 로그만 기록
  • 성능 최적화: Promise.all로 병렬 파일 삭제 처리
  • 에러 격리: 파일 삭제 실패가 API 성공에 영향 미치지 않음

파일 제한사항

  • 이미지: 최대 10개, 각 10MB, 지원 형식: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif
  • 영상: 최대 5개, 각 100MB, 지원 형식: .mp4, .avi, .mov, .wmv, .flv
  • 썸네일: 영상 파일에 대해 자동 생성


4. 피드 상세 조회

4. 피드 상세 조회

GET /feeds/{feedId}

특정 피드의 상세 정보를 조회합니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID

Request Example

GET /feeds/1

Response Example

{
  "code": 200,
  "message": "피드를 조회했습니다.",
  "data": {
    "id": 1,
    "created_at": "2024-01-15T10:30:00.000Z",
    "updated_at": "2024-01-15T10:30:00.000Z",
    "user": {
      "id": 1,
      "nickname": "사용자닉네임",
      "profile_img": "http://localhost:3001/uploads/profile.jpg"
    },
    "content_blocks": [
      {
        "id": 1,
        "type": "text",
        "value": "피드 텍스트 내용입니다.",
        "sequence": 0
      },
      {
        "id": 2,
        "type": "image",
        "value": "http://localhost:3001/uploads/2024/01/15/image_123.jpg",
        "sequence": 1,
        "path": "/uploads/2024/01/15/image_123.jpg"
      }
    ],
      "media_count": 2,
      "like_count": 12,
      "bookmark_count": 3,
      "comment_count": 5,
      "is_liked": true,
      "is_bookmarked": false,
      "is_author": true
  }
}

Response Fields

Root Data Object:

Field Type Required Description
id number 필수 피드 고유 ID
created_at string 필수 피드 작성 시간 (ISO 8601)
updated_at string 필수 피드 수정 시간 (ISO 8601)
user object 필수 작성자 정보
content_blocks array 필수 콘텐츠 블록 배열
media_count number 필수 미디어 블록 총 개수 (이미지/영상)
like_count number 필수 좋아요 수
bookmark_count number 필수 북마크 수
comment_count number 필수 댓글 수
is_liked boolean 필수 사용자 좋아요 여부
is_bookmarked boolean 필수 사용자 북마크 여부
is_author boolean 필수 피드 작성자 여부

User Object:

Field Type Required Description
id number 필수 사용자 고유 ID
nickname string 필수 사용자 닉네임
profile_img string? 선택 프로필 이미지 URL

Content Block Object:

Field Type Required Description
id number 필수 블록 고유 ID
type string 필수 블록 타입 ("text", "image", "video")
value string 필수 블록 내용 (전체 URL)
sequence number 필수 블록 순서
path string? 조건부 상대 경로 (image/video 타입일 때만 제공)
thumbnail_value string? 조건부 썸네일 전체 URL (video 타입일 때만 제공)
thumbnail_path string? 조건부 썸네일 상대 경로 (video 타입일 때만 제공)

주의사항:

  • is_liked, is_bookmarked: 로그인한 사용자에게만 제공됩니다.
  • profile_img: 사용자가 프로필 이미지를 설정하지 않은 경우 null입니다.

5. 피드 삭제

DELETE /feeds/{feedId}

피드를 삭제합니다. 작성자 본인만 삭제 가능하며, 연결된 모든 블록과 상호작용 데이터도 함께 삭제됩니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID

Request Example

DELETE /feeds/1

Response Example

{
  "code": 200,
  "message": "피드가 삭제되었습니다.",
  "data": null
}

Response Fields

Root Data Object: null

주의사항:

  • 피드 삭제 시 다음 데이터도 함께 삭제됩니다:
    • FeedPostContent (콘텐츠 블록)
    • FeedLike (좋아요)
    • FeedBookmark (북마크)
    • FeedComment (댓글)

랜덤 인기 피드 조회 API

랜덤 인기 피드 조회

GET /feeds/random

한 달 내 작성된 공개 프로필 사용자의 피드 중 인기도가 높은 피드들을 대상으로 랜덤하게 하나를 반환합니다. 피드 다양성 확보를 위한 용도로, 호출할 때마다 이전에 받은 피드와 중복되지 않도록 제외 ID를 지정할 수 있습니다.

Query Parameters

파라미터 타입 필수 기본값 설명
exclude_ids string 선택 null 제외할 피드 ID 목록 (쉼표로 구분, 최대 50개)

Request Example

# 첫 번째 호출
GET /feeds/random

# 중복 방지 적용
GET /feeds/random?exclude_ids=123,456,789

# 다수의 제외 ID
GET /feeds/random?exclude_ids=1,2,3,4,5,6,7,8,9,10
// 랜덤 인기 피드 조회 API 호출
const response = await fetch('/feeds/random', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

const data = await response.json();
console.log('랜덤 인기 피드:', data.data);

Processing Flow

1단계: 파라미터 검증

exclude_ids 파싱 및 유효성 검증
→ 최대 50개 ID까지만 허용 (과도한 쿼리 방지)
→ 숫자형 ID로 변환 및 검증

2단계: 전체 공개 피드 수 확인

한 달 내 공개 프로필 사용자 피드 개수 조회
→ 동적 후보 수 계산: MAX(MIN(total_count * 0.2, 50), 5)

3단계: 인기 피드 후보 추출

engagement_score = like_count + (comment_count * 1.5) + (bookmark_count * 2)
→ 상위 N개 후보 추출 (N = 동적 계산값)
→ exclude_ids 필터링 적용

4단계: 랜덤 선택 및 상세 조회

후보 중 랜덤으로 하나 선택
→ 선택된 피드의 상세 정보 조회
→ 사용자 상호작용 상태 포함

인기도 계산 상세

Engagement Score 계산:

// 좋아요 1점, 댓글 1.5점, 북마크 2점으로 가중치 적용
engagement_score = like_count + (comment_count * 1.5) + (bookmark_count * 2.0)

// 시간 가중치 (선택적)
time_weight = EXP(-DATEDIFF(NOW(), created_at) / 30)  // 30일 반감기
final_score = engagement_score * time_weight

동적 후보 수 계산:

totalPublicFeeds = await countPublicFeedsLastMonth();
candidateCount = Math.min(
  Math.max(Math.floor(totalPublicFeeds * 0.2), 5),  // 최소 5개
  50  // 최대 50개
);

Response Example

성공 응답 (랜덤 인기 피드 반환):

{
  "code": 200,
  "message": "랜덤 인기 피드를 조회했습니다.",
  "data": {
    "id": 456,
    "created_at": "2025-01-20T14:22:00.000Z",
    "user": {
      "id": 25,
      "nickname": "popular_creator",
      "profile_img": "https://cdn.example.com/profiles/user25.jpg"
    },
    "content_blocks": [
      {
        "id": 789,
        "type": "text",
        "value": "인기 피드 내용입니다!",
        "sequence": 0
      },
      {
        "id": 790,
        "type": "image",
        "value": "https://cdn.example.com/feeds/456/image1.jpg",
        "sequence": 1,
        "path": "/uploads-feed/2025/01/20/image1.jpg"
      }
    ],
    "media_count": 1,
    "like_count": 89,
    "bookmark_count": 12,
    "comment_count": 23,
    "is_liked": false,
    "is_bookmarked": false,
    "is_author": false
  }
}

성공 응답 (후보 없음):

{
  "code": 200,
  "message": "랜덤 인기 피드를 조회했습니다.",
  "data": null
}

Response Fields Details

Data Object (FeedDetail):

Field Type Required Description
id number 필수 피드 고유 ID
created_at string 필수 피드 작성 시간 (ISO 8601)
updated_at string 필수 피드 수정 시간 (ISO 8601)
user object 필수 작성자 정보
content_blocks array 필수 콘텐츠 블록 배열
media_count number 필수 미디어 블록 총 개수
like_count number 필수 좋아요 수
bookmark_count number 필수 북마크 수
comment_count number 필수 댓글 수
is_liked boolean 필수 사용자 좋아요 상태
is_bookmarked boolean 필수 사용자 북마크 상태
is_author boolean 필수 사용자의 소유권 여부

Validation Rules

Query Parameters:

  • exclude_ids:
    • 쉼표로 구분된 숫자 목록
    • 각 ID는 1 이상의 정수
    • 최대 50개 ID까지만 허용
    • 형식: 1,2,3,4,5

필터링 조건:

  • 한 달 내 작성된 피드 (created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY))
  • 공개 프로필 사용자 (User.profile_visibility = 'public')
  • 제외 ID 필터링 (id NOT IN (excludeIds))

Performance Optimization

데이터베이스 최적화:

  • 3단계 쿼리: COUNT → 후보 ID 조회 → 상세 조회
  • INDEX 활용: FeedPost(created_at), User(profile_visibility)
  • 배치 처리: 후보 ID 목록으로 단일 상세 조회
  • N+1 방지: JOIN 쿼리로 최적화

쿼리 비용 절감:

  • 동적 LIMIT으로 불필요한 정렬 방지
  • 후보 수 제한 (최대 50개)으로 메모리 사용 최적화
  • exclude_ids로 IN 조건 최적화

Error Response Examples

잘못된 exclude_ids 형식:

{
  "code": 400,
  "message": "exclude_ids는 숫자 목록이어야 합니다.",
  "errors": [
    {
      "field": "exclude_ids",
      "message": "exclude_ids는 숫자 목록이어야 합니다."
    }
  ]
}

exclude_ids가 너무 많음:

{
  "code": 400,
  "message": "exclude_ids는 최대 50개까지만 허용됩니다.",
  "errors": [
    {
      "field": "exclude_ids",
      "message": "exclude_ids는 최대 50개까지만 허용됩니다."
    }
  ]
}

인증 실패:

{
  "code": 401,
  "message": "인증이 필요합니다.",
  "errors": []
}

Technical Implementation Details

쿼리 단계별 구현:

// 1단계: 전체 공개 피드 수 확인
async getPublicFeedsCountLastMonth(excludeIds: number[]): Promise<number> {
  const result = await sequelize.query(
    `SELECT COUNT(*) as total FROM FeedPost f
     JOIN User u ON f.user_id = u.id
     WHERE f.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
       AND u.profile_visibility = 'public'
       AND f.id NOT IN (?)`,
    { replacements: [excludeIds] }
  );
  return result[0].total;
}

// 2단계: 인기 후보 추출
async getTopPopularFeedCandidateIds(limit: number, excludeIds: number[]): Promise<number[]> {
  const result = await sequelize.query(
    `SELECT f.id
     FROM FeedPost f
     JOIN User u ON f.user_id = u.id
     WHERE f.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
       AND u.profile_visibility = 'public'
       AND f.id NOT IN (?)
     ORDER BY (f.like_count + f.comment_count * 1.5 + f.bookmark_count * 2) DESC
     LIMIT ?`,
    { replacements: [excludeIds, limit] }
  );
  return result.map(row => row.id);
}

// 3단계: 랜덤 선택 + 상세 조회
const selectedId = candidateIds[Math.floor(Math.random() * candidateIds.length)];
return await this.getFeedById(selectedId, userId);

클라이언트 중복 방지 구현:

class RandomFeedManager {
  private seenIds = new Set<number>();

  async fetchRandomPopularFeed(): Promise<FeedDetail | null> {
    const excludeIds = Array.from(this.seenIds).join(',');
    const query = excludeIds ? `?exclude_ids=${excludeIds}` : '';

    const response = await fetch(`/feeds/random${query}`);
    const data = await response.json();

    if (data.data) {
      this.seenIds.add(data.data.id);
      // 최대 100개 유지
      if (this.seenIds.size > 100) {
        this.seenIds = new Set(Array.from(this.seenIds).slice(-50));
      }
    }

    return data.data;
  }
}

Usage Scenario

피드 다양성 확보:

async function loadFeedWithRandomInsertions(feedCursor?: number) {
  // 일반 피드 9개 로드
  const feedResponse = await fetch(`/feeds?cursor=${feedCursor || ''}&limit=9`);
  const feedData = await feedResponse.json();

  // 랜덤 인기 피드 1개 로드
  const randomManager = new RandomFeedManager();
  const randomFeed = await randomManager.fetchRandomPopularFeed();

  // 9개 피드 + 1개 랜덤 = 10개 조합
  const combinedFeeds = [
    ...feedData.data.feeds.slice(0, 5),  // 앞 5개
    randomFeed,                          // 중간에 랜덤 인기 피드 삽입
    ...feedData.data.feeds.slice(5)      // 나머지 4개
  ].filter(Boolean); // null 제거

  return {
    feeds: combinedFeeds,
    pagination: feedData.data.pagination
  };
}

이 API는 피드 다양성 확보를 위한 핵심 기능으로, 인기도 기반으로 검증된 콘텐츠를 랜덤하게 제공합니다. 동적 후보 수 계산과 N+1 문제 방지로 최적화된 성능을 유지합니다.


6. 피드 목록 조회

GET /feeds

피드 목록을 커서 기반 페이지네이션으로 조회합니다. 최신순으로 정렬됩니다.

Query Parameters

파라미터 타입 필수 설명
cursor number 선택 마지막으로 조회한 피드 ID (다음 페이지 조회용)
limit number 선택 조회할 항목 수 (기본값: 20, 최대: 50)

Request Example

GET /feeds
GET /feeds?cursor=10&limit=15

Response Example

{
  "code": 200,
  "message": "피드 목록을 조회했습니다.",
  "data": {
    "feeds": [
      {
        "id": 15,
        "created_at": "2024-01-15T12:30:00.000Z",
        "user": {
          "id": 2,
          "nickname": "다른사용자",
          "profile_img": "http://localhost:3001/uploads/profile2.jpg"
        },
        "content_blocks": [
          {
            "type": "text",
            "value": "피드 텍스트 내용",
            "sequence": 0
          },
          {
            "type": "image",
            "value": "http://localhost:3001/uploads/2024/01/15/image_456.jpg",
            "sequence": 1,
            "path": "/uploads/2024/01/15/image_456.jpg"
          },
          {
            "type": "image",
            "value": "http://localhost:3001/uploads/2024/01/15/image_789.jpg",
            "sequence": 2,
            "path": "/uploads/2024/01/15/image_789.jpg"
          },
          {
            "type": "video",
            "value": "http://localhost:3001/uploads/2024/01/15/video_123.mp4",
            "path": "/uploads/2024/01/15/video_123.mp4",
            "thumbnail_value": "http://localhost:3001/uploads-feed-video-thumbnail/2024/01/15/thumbnail_video_123.jpg",
            "thumbnail_path": "/uploads-feed-video-thumbnail/2024/01/15/thumbnail_video_123.jpg",
            "sequence": 3
          }
        ],
        "media_count": 3,
        "like_count": 8,
        "bookmark_count": 2,
        "comment_count": 3,
        "is_liked": false,
        "is_bookmarked": true,
        "is_author": false
      }
    ],
    "pagination": {
      "next_cursor": 10,
      "has_next": true
    }
  }
}

Response Fields

Root Data Object:

Field Type Required Description
feeds array 필수 피드 목록 배열
pagination object 필수 페이지네이션 정보

Feed List Item Object:

Field Type Required Description
id number 필수 피드 고유 ID
created_at string 필수 피드 작성 시간 (ISO 8601)
user object 필수 작성자 정보
content_blocks array 필수 콘텐츠 블록 배열
media_count number 필수 미디어 블록 총 개수 (이미지/영상)
like_count number 필수 좋아요 수
bookmark_count number 필수 북마크 수
comment_count number 필수 댓글 수
is_liked boolean 필수 사용자 좋아요 여부
is_bookmarked boolean 필수 사용자 북마크 여부

Pagination Object:

Field Type Required Description
next_cursor number? 선택 다음 페이지 조회용 커서
has_next boolean 필수 다음 페이지 존재 여부

주의사항:

  • next_cursor: 다음 페이지가 없는 경우 null입니다.
  • 커서 기반 페이지네이션으로 무한 스크롤을 지원합니다.

7. 피드 좋아요 토글

POST /feeds/{feedId}/like

피드에 좋아요를 추가하거나 취소합니다. 이미 좋아요를 누른 경우 취소됩니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID

Request Example

POST /feeds/1/like

Response Example

{
  "code": 200,
  "message": "피드를 좋아요했습니다.",
  "data": {
    "is_liked": true,
    "like_count": 13
  }
}

Response Fields

Root Data Object:

Field Type Required Description
is_liked boolean 필수 좋아요 상태
like_count number 필수 업데이트된 좋아요 수

주의사항:

  • 좋아요 추가 시: is_liked: true, like_count 증가
  • 좋아요 취소 시: is_liked: false, like_count 감소

8. 피드 북마크 토글

POST /feeds/{feedId}/bookmark

피드를 북마크하거나 취소합니다. 이미 북마크한 경우 취소됩니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID

Request Example

POST /feeds/1/bookmark

Response Example

{
  "code": 200,
  "message": "피드를 북마크했습니다.",
  "data": {
    "is_bookmarked": true,
    "bookmark_count": 4
  }
}

Response Fields

Root Data Object:

Field Type Required Description
is_bookmarked boolean 필수 북마크 상태
bookmark_count number 필수 업데이트된 북마크 수

주의사항:

  • 북마크 추가 시: is_bookmarked: true, bookmark_count 증가
  • 북마크 취소 시: is_bookmarked: false, bookmark_count 감소

9. 피드 댓글 작성

POST /feeds/{feedId}/comments

피드에 댓글을 작성합니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID

Request Body

{
  "content": "댓글 내용입니다.",
  "parent_comment_id": 5
}

Field Descriptions

필드 타입 필수 설명
content string 필수 댓글 내용 (1-1000자)
parent_comment_id number 선택 부모 댓글 ID (대댓글인 경우)
mention_user_id number 선택 언급할 사용자 ID (멘션 기능)

Validation Rules

  • content: 1-1000자 길이 제한
  • parent_comment_id:
    • 선택사항
    • 대댓글인 경우 유효한 댓글 ID여야 함

Request Example

// 일반 댓글
{
  "content": "정말 멋진 피드네요!"
}

// 대댓글
{
  "content": "동감합니다!",
  "parent_comment_id": 5
}

Response Example

{
  "code": 201,
  "message": "댓글이 작성되었습니다.",
  "data": {
    "commentId": 10,
    "created_at": "2024-01-15T16:30:00.000Z"
  }
}

Response Fields

Root Data Object:

Field Type Required Description
commentId number 필수 생성된 댓글 고유 ID
created_at string 필수 작성 시간 (ISO 8601)

10. 피드 댓글 목록 조회

GET /feeds/{feedId}/comments

특정 피드의 댓글 목록을 커서 기반 페이지네이션으로 조회합니다. 대댓글은 부모 댓글의 replies 배열에 포함되어 반환되며, 최적화된 쿼리로 성능이 향상되었습니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 댓글을 조회할 피드의 고유 ID

Query Parameters

파라미터 타입 필수 설명
cursor string 선택 마지막으로 조회한 댓글 ID (다음 페이지 조회용)
limit number 선택 조회할 항목 수 (기본값: 20, 최대: 50)

Headers

헤더명 설명 필수
Authorization Bearer 토큰 (JWT) 필수

Response Example (200 OK)

{
  "code": 200,
  "message": "댓글 목록을 조회했습니다.",
  "data": {
    "items": [
      {
        "id": 5,
        "content": "정말 멋진 글이네요!",
        "created_at": "2024-01-15T10:30:00.000Z",
        "updated_at": "2024-01-15T10:30:00.000Z",
        "user": {
          "id": 2,
          "nickname": "user2",
          "profile_img": "http://localhost:3001/uploads/profile2.jpg"
        },
        "parent_comment_id": null,
        "mention_user": null,
        "like_count": 3,
        "is_liked": true,
        "is_author": false,
        "is_deleted": false,
        "replies": [
          {
            "id": 10,
            "content": "동감합니다!",
            "created_at": "2024-01-15T11:30:00.000Z",
            "updated_at": "2024-01-15T11:30:00.000Z",
            "user": {
              "id": 3,
              "nickname": "user3",
              "profile_img": "http://localhost:3001/uploads/profile3.jpg"
            },
            "parent_comment_id": 5,
            "mention_user": {
              "id": 2,
              "nickname": "user2"
            },
            "like_count": 1,
            "is_liked": false,
            "is_author": false,
            "is_deleted": false
          }
        ]
      }
    ],
    "next_cursor": "100"
  }
}

Response Fields

Root Data Object:

필드 타입 필수 설명
items array 필수 댓글 목록 배열
next_cursor string|null 필수 다음 페이지 조회용 커서

Item Object (댓글 및 대댓글 공통):

필드 타입 필수 설명
id number 필수 댓글 고유 ID
content string 필수 댓글 내용
created_at string 필수 작성 시간 (ISO 8601)
updated_at string 필수 수정 시간 (ISO 8601)
user object 필수 작성자 정보 객체
user.id number 필수 작성자 ID
user.nickname string 필수 작성자 닉네임
user.profile_img string|null 선택 작성자 프로필 이미지 URL
parent_comment_id number|null 선택 부모 댓글 ID (대댓글인 경우)
mention_user object|null 선택 멘션 대상 사용자 정보 ({ id, nickname })
like_count number 필수 좋아요 수
is_liked boolean 필수 현재 사용자 기준 좋아요 여부
is_author boolean 필수 현재 사용자 기준 작성자 여부
is_deleted boolean 필수 삭제된 댓글 여부 (삭제 시 내용/작성자 마스킹)
replies array 선택 대댓글 목록 (동일 구조의 배열)

Page सूस Parameters Examples

첫 페이지 조회:

GET /feeds/1/comments

두 번째 페이지 조회:

GET /feeds/1/comments?cursor=100&limit=30

Validation Rules

  • feedId: 유효한 피드 ID
  • cursor: 유효한 숫자 문자열 또는 null
  • limit: 1-50 사이의 정수 (기본값: 20)

Performance Optimizations

  • 병렬 쿼리 처리: 좋아요 카운트와 사용자 좋아요 상태를 동시에 조회
  • 속성 제한: 필요없는 데이터베이스 컬럼은 조회하지 않음
  • 커서 기반 페이지네이션: ID 기반으로 빠른 페이지네이션
  • N+1 문제 해결: 최적화된 쿼리로 한번에 모든 데이터 조회

Error Response Examples

유효하지 않은 cursor:

{
  "code": 400,
  "message": "Invalid cursor",
  "errors": []
}

limit 범위를 초과:

{
  "code": 400,
  "message": "Validation failed",
  "errors": [
    {
      "field": "limit",
      "message": "limit must be between 1 and 50"
    }
  ]
}

11. 피드 댓글 수정

PATCH /feeds/{feedId}/comments/{commentId}

선택에 따라 댓글의 내용을 수정합니다. 오직 댓글 작성자만 수정할 수 있습니다.

파라미터 설명

  • feedId (필수): 댓글의 피드 ID
  • commentId (필수): 수정할 댓글 ID

요청 본문

{
  "content": "수정할 댓글 내용"
}

유효성 검사

  • 댓글 내용은 1자 이상, 1000자 이하여야 합니다
  • 작성자만 댓글을 수정할 수 있습니다
  • 존재하지 않는 댓글은 수정할 수 없습니다
  • 논리적으로 삭제된 댓글은 수정할 수 없습니다

응답 예시

{
  "code": 200,
  "message": "댓글이 수정되었습니다.",
  "data": {
    "id": 10,
    "updated_at": "2024-01-15T16:45:00.000Z"
  }
}

오류 상황

  • 댓글 존재하지 않음: 404 Not Found
  • 수정 권한 없음: 403 Forbidden
  • 유효하지 않은 내용: 400 Bad Request
  • 삭제된 댓글: 400 Bad Request

13. 피드 댓글 삭제 (소프트 삭제)

DELETE /feeds/{feedId}/comments/{commentId}

피드 댓글을 소프트 삭제합니다. 작성자 본인만 삭제 가능합니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID
commentId number 필수 댓글 ID

Request Example

DELETE /feeds/1/comments/10

Response Example

{
  "code": 200,
  "message": "댓글이 삭제되었습니다.",
  "data": null
}

Response Fields

Root Data Object: null

주의사항 (소프트 삭제 동작):

  • 댓글은 물리적으로 삭제되지 않으며 is_deleted: true로 표시됩니다.
  • 삭제된 댓글은 내용이 "삭제된 댓글입니다."로 마스킹되고, 작성자 정보는 { id: 0, nickname: "알 수 없음", profile_img: null }로 대체됩니다.
  • 삭제된 댓글의 like_count는 0, is_liked/is_author는 false, mention_user는 null로 반환됩니다.
  • 대댓글(자식 댓글)은 유지됩니다.

Error Response Examples

댓글을 찾을 수 없음:

{
  "code": 404,
  "message": "댓글을 찾을 수 없습니다.",
  "errors": []
}

수정 권한 없음:

{
  "code": 403,
  "message": "댓글 삭제 권한이 없습니다.",
  "errors": []
}

잘못된 피드의 댓글:

{
  "code": 400,
  "message": "잘못된 피드의 댓글입니다.",
  "errors": []
}

+++

피드 댓글 삭제 API 수정 완료! 이제 올바르게 DELETE 메소드로 표기되었습니다.



13. 피드 댓글 삭제 (소프트 삭제)

DELETE /feeds/{feedId}/comments/{commentId}

피드 댓글을 소프트 삭제합니다. 작성자 본인만 삭제 가능합니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID
commentId number 필수 댓글 ID

Request Example

DELETE /feeds/1/comments/10

Response Example

{
  "code": 200,
  "message": "댓글이 삭제되었습니다.",
  "data": null
}

Response Fields

Root Data Object: null

주의사항 (소프트 삭제 동작):

  • 댓글은 물리적으로 삭제되지 않으며 is_deleted: true로 표시됩니다.
  • 삭제된 댓글은 내용이 "삭제된 댓글입니다."로 마스킹되고, 작성자 정보는 { id: 0, nickname: "알 수 없음", profile_img: null }로 대체됩니다.
  • 삭제된 댓글의 like_count는 0, is_liked/is_author는 false, mention_user는 null로 반환됩니다.
  • 대댓글(자식 댓글)은 유지됩니다.

14. 피드 댓글 좋아요 토글

POST /feeds/{feedId}/comments/{commentId}/like

피드 댓글에 좋아요를 추가하거나 취소합니다. 이미 좋아요를 누른 경우 취소됩니다.

Path Parameters

파라미터 타입 필수 설명
feedId number 필수 피드 ID
commentId number 필수 댓글 ID

Request Example

POST /feeds/1/comments/10/like

Response Example

{
  "code": 200,
  "message": "댓글을 좋아요했습니다.",
  "data": {
    "is_liked": true,
    "like_count": 5
  }
}

Response Fields

Root Data Object:

Field Type Required Description
is_liked boolean 필수 좋아요 상태
like_count number 필수 업데이트된 좋아요 수

상태 변경

현재 상태 액션 응답 메시지 is_liked like_count 변화
좋아요 안 함 좋아요 추가 "댓글을 좋아요했습니다." true +1
좋아요 함 좋아요 취소 "댓글 좋아요를 취소했습니다." false -1

Validation Rules

  • feedId, commentId: 유효한 숫자여야 함
  • 게시글 존재 여부: 존재하지 않는 피드를 지정할 경우 404 Not Found
  • 댓글 존재 여부: 존재하지 않는 댓글을 지정할 경우 404 Not Found
  • 소프트 삭제된 댓글: 삭제된 댓글에는 좋아요 불가 (400 Bad Request)

Error Response Examples

존재하지 않는 피드:

{
  "code": 404,
  "message": "피드를 찾을 수 없습니다.",
  "errors": []
}

존재하지 않는 댓글:

{
  "code": 404,
  "message": "댓글을 찾을 수 없습니다.",
  "errors": []
}

삭제된 댓글에 좋아요 시도:

{
  "code": 400,
  "message": "삭제된 댓글에는 좋아요를 할 수 없습니다.",
  "errors": []
}

15. 피드 북마크 목록 조회

GET /feeds/bookmarks

사용자가 북마크한 피드 목록을 오프셋 기반 페이지네이션으로 조회합니다. 최신 북마크순으로 정렬됩니다.

Query Parameters

파라미터 타입 필수 설명
offset number 선택 건너뛸 항목 수 (기본값: 0)
limit number 선택 조회할 항목 수 (기본값: 20, 최대: 100)

Request Example

GET /feeds/bookmarks
GET /feeds/bookmarks?offset=20&limit=15

Response Example

{
  "code": 200,
  "message": "북마크한 피드 목록을 조회했습니다.",
  "data": {
    "feeds": [
      {
        "id": 15,
        "created_at": "2024-01-15T12:30:00.000Z",
        "preview_image": "http://localhost:3001/uploads-feed/2024/01/15/image_456.jpg",
        "preview_content_type": "image"
      },
      {
        "id": 8,
        "created_at": "2024-01-14T09:15:00.000Z",
        "preview_image": "http://localhost:3001/uploads-feed-video-thumbnail/2024/01/14/thumbnail_video_123.jpg",
        "preview_content_type": "video"
      }
    ],
    "pagination": {
      "offset": 0,
      "limit": 20,
      "total": 45,
      "has_next": true
    }
  }
}

Response Fields

Root Data Object:

Field Type Required Description
feeds array 필수 북마크한 피드 목록 배열
pagination object 필수 페이지네이션 정보

Feed List Item Object:

Field Type Required Description
id number 필수 피드 고유 ID
created_at string 필수 피드 작성 시간 (ISO 8601)
preview_image string? 선택 첫 번째 콘텐츠의 이미지 또는 비디오 썸네일 URL
preview_content_type string? 선택 미리보기 콘텐츠 타입 ("image" 또는 "video")

Pagination Object:

Field Type Required Description
offset number 필수 현재 오프셋
limit number 필수 조회 제한 수
total number 필수 전체 북마크 수
has_next boolean 필수 다음 페이지 존재 여부

Validation Rules

  • offset: 0 이상의 정수 (기본값: 0)
  • limit: 1-100 사이의 정수 (기본값: 20)

Performance Optimizations

  • Single Query with Join: FeedBookmark와 FeedPost를 join하여 N+1 문제 해결
  • Content Extraction: FeedPostContent에서 미리보기 이미지/썸네일 추출
  • Indexing: FeedBookmark.user_id + FeedBookmark.created_at 인덱스 활용

Error Response Examples

offset 범위를 초과:

{
  "code": 400,
  "message": "offset은 0 이상의 숫자여야 합니다.",
  "errors": []
}

limit 범위를 초과:

{
  "code": 400,
  "message": "limit은 1-100 사이의 값이어야 합니다.",
  "errors": []
}

오류 코드

HTTP 상태 코드 설명
400 Bad Request 잘못된 요청 (유효성 검사 실패)
401 Unauthorized 인증되지 않은 요청
403 Forbidden 권한이 없는 요청 (작성자가 아닌 경우)
404 Not Found 리소스를 찾을 수 없음
409 Conflict 중복된 요청 (이미 좋아요한 피드 등)
500 Internal Server Error 서버 내부 오류

인증

모든 API는 JWT 토큰을 통한 인증이 필요합니다.

헤더 방식 (모바일 앱)

Authorization: Bearer YOUR_JWT_TOKEN

쿠키 방식 (웹)

  • accessToken 쿠키가 자동으로 전송됩니다.
  • HttpOnly 쿠키로 설정되어 있어 JavaScript에서 접근할 수 없습니다.

유효성 검사 규칙

콘텐츠 블록

  • 최소 블록 수: 1개 (텍스트 블록 필수)
  • 최대 블록 수: 10개
  • 텍스트 블록:
    • 반드시 1개만 존재
    • sequence는 반드시 0
    • 길이: 1-1000자
  • 이미지/영상 블록:
    • 최대 9개
    • sequence는 1부터 순차적으로 증가
    • 유효한 파일 URL 형식

댓글

  • 길이: 1-1000자

페이지네이션

  • limit: 1-50 사이의 정수 (기본값: 20)
  • 커서: 유효한 피드 ID 또는 null

사용 예시

피드 생성

// 텍스트 + 이미지 피드 생성
const response = await fetch('/feeds', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    content_blocks: [
      {
        type: 'text',
        value: '오늘 점심 메뉴입니다!',
        sequence: 0
      },
      {
        type: 'image',
        value: 'http://localhost:3001/uploads/2024/01/15/lunch.jpg',
        sequence: 1
      }
    ]
  })
});

const result = await response.json();
console.log('생성된 피드 ID:', result.data.feedId);

피드 목록 조회 (무한 스크롤)

// 첫 페이지 조회
let response = await fetch('/feeds?limit=20', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

let data = await response.json();
let feeds = data.data.feeds;
let nextCursor = data.data.pagination.next_cursor;

// 다음 페이지 조회
while (nextCursor) {
  response = await fetch(`/feeds?cursor=${nextCursor}&limit=20`, {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  
  data = await response.json();
  feeds = feeds.concat(data.data.feeds);
  nextCursor = data.data.pagination.next_cursor;
}

console.log('전체 피드 수:', feeds.length);

피드 좋아요 토글

const response = await fetch('/feeds/1/like', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

const result = await response.json();
if (result.data.is_liked) {
  console.log('좋아요 완료!');
} else {
  console.log('좋아요 취소!');
}

성능 최적화

데이터베이스 인덱스

  • FeedPost.id (Primary Key)
  • FeedPost.user_id
  • FeedPost.created_at (DESC)
  • FeedPostContent.feed_post_id + FeedPostContent.sequence
  • FeedLike.feed_post_id + FeedLike.user_id
  • FeedBookmark.feed_post_id + FeedBookmark.user_id
  • FeedComment.feed_post_id + FeedComment.created_at

캐싱 전략

  • 피드 목록: 30초 캐시
  • 피드 상세: 1분 캐시
  • 좋아요/북마크 상태: 5분 캐시

페이지네이션 최적화

  • 커서 기반 페이지네이션으로 성능 향상
  • 인덱스를 활용한 빠른 조회
  • 최대 50개까지 한 번에 조회 가능

파일 업로드

피드에 사용할 이미지/영상은 별도 업로드 API를 통해 먼저 업로드해야 합니다.

피드 이미지 업로드

  • API: POST /feeds/upload/images
  • 지원 형식: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif
  • 최대 크기: 10MB
  • 최대 개수: 10개
  • 저장 경로: uploads-feed/년/월/일/

피드 영상 업로드

  • API: POST /feeds/upload/video
  • 지원 형식: .mp4, .avi, .mov, .wmv, .flv
  • 최대 크기: 100MB
  • 최대 개수: 1개
  • 저장 경로: uploads-feed-video/년/월/일/
  • 썸네일 경로: uploads-feed-video-thumbnail/년/월/일/
  • 썸네일 자동 생성: 영상의 50% 지점에서 추출

업로드 플로우

1. 파일 선택
2. 피드 파일 업로드 API 호출
3. 반환받은 URL을 피드 생성 시 사용
4. 피드 생성