Skip to content

Latest commit

 

History

History
2012 lines (1711 loc) · 55.6 KB

File metadata and controls

2012 lines (1711 loc) · 55.6 KB

Post 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. 게시글 목록 조회

GET /posts

게시글 목록을 페이지네이션으로 조회합니다.

Query Parameters

파라미터 타입 필수 설명
categoryId number 선택 카테고리 ID
subCategoryId number 선택 서브카테고리 ID
animalType string 선택 동물 타입 ('dog', 'cat', 'small_pet', 'bird', 'reptile', 'fish', 'other')
page number 선택 페이지 번호 (기본값: 1)

Request Example

GET /posts?categoryId=1&page=1
GET /posts?subCategoryId=3&page=2

Response Example

{
  "code": 200,
  "message": "게시글 목록을 조회했습니다.",
  "data": {
    "posts": [
      {
        "id": 1,
        "title": "게시글 제목",
        "animal_type": "dog",
        "content": "게시글의 첫 번째 텍스트 내용 미리보기...",
        "created_at": "2024-01-15T10:30:00.000Z",
        "user": {
          "id": 1,
          "nickname": "사용자닉네임",
          "profile_img": "http://localhost:3001/uploads/profile.jpg"
        },
        "sub_category": {
          "id": 1,
          "name": "자유게시판",
          "category": {
            "id": 1,
            "name": "일반"
          }
        },
        "like_count": 5,
        "comment_count": 3,
        "preview_image": "http://localhost:3001/uploads-post-video-thumbnail/2024/01/15/thumbnail_video_123.jpg",
        "is_liked": true,
        "is_bookmarked": false
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 100,
      "total_pages": 5,
      "has_next": true,
      "has_prev": false
    }
  }
}

Response Fields

Root Data Object:

Field Type Required Description
posts array 필수 게시글 목록 배열
pagination object 필수 페이지네이션 정보

Post List Item Object:

Field Type Required Description
id number 필수 게시글 고유 ID
title string 필수 게시글 제목
content string 필수 게시글의 첫 번째 텍스트 콘텐츠
created_at string 필수 게시글 작성 시간 (ISO 8601)
user object 필수 작성자 정보
sub_category object 필수 서브카테고리 정보
like_count number 필수 좋아요 수
comment_count number 필수 댓글 수
bookmark_count number 필수 북마크 수
preview_image string? 선택 미리보기 이미지 URL (첫 번째 이미지 또는 비디오 썸네일)
is_liked boolean? 선택 사용자 좋아요 여부 (로그인 시에만 제공)
is_bookmarked boolean? 선택 사용자 북마크 여부 (로그인 시에만 제공)

Pagination Object:

Field Type Required Description
page number 필수 현재 페이지 번호
limit number 필수 페이지당 항목 수
total number 필수 전체 항목 수
total_pages number 필수 전체 페이지 수
has_next boolean 필수 다음 페이지 존재 여부
has_prev boolean 필수 이전 페이지 존재 여부

주의사항:

  • preview_image: 게시글에 이미지가 없는 경우 null입니다.
  • is_liked, is_bookmarked: 로그인한 사용자에게만 제공됩니다.

2. 게시글 상세 조회

GET /posts/{postId}

특정 게시글의 상세 정보를 조회합니다.

Path Parameters

파라미터 타입 필수 설명
postId number 필수 게시글 ID

Request Example

GET /posts/1

Response Example

{
  "code": 200,
  "message": "게시글을 조회했습니다.",
  "data": {
    "id": 1,
    "title": "게시글 제목",
    "animal_type": "dog",
    "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"
    },
    "sub_category": {
      "id": 1,
      "name": "자유게시판",
      "category": {
        "id": 1,
        "name": "일반"
      }
    },
    "content_blocks": [
      {
        "type": "text",
        "value": "게시글 텍스트 내용입니다.",
        "sequence": 0
      },
      {
        "type": "image",
        "value": "http://localhost:3001/uploads/2024/01/15/image_123.jpg",
        "sequence": 1,
        "path": "/uploads/2024/01/15/image_123.jpg"
      },
      {
        "type": "video",
        "value": "http://localhost:3001/uploads-post-video/2024/01/15/video_123.mp4",
        "sequence": 2,
        "thumbnail_path": "http://localhost:3001/uploads-post-video-thumbnail/2024/01/15/thumbnail_video_123.jpg"
      }
    ],
    "tags": [
      {
        "id": 1,
        "name": "태그1"
      },
      {
        "id": 2,
        "name": "태그2"
      }
    ],
    "like_count": 5,
    "bookmark_count": 2,
    "comment_count": 3,
    "is_liked": true,
    "is_bookmarked": false,
    "is_author": true
  }
}

Response Fields

Root Data Object:

Field Type Required Description
id number 필수 게시글 고유 ID
title string 필수 게시글 제목
created_at string 필수 게시글 작성 시간 (ISO 8601)
updated_at string 필수 게시글 수정 시간 (ISO 8601)
user object 필수 작성자 정보
sub_category object 필수 서브카테고리 정보
content_blocks array 필수 게시글 내용 블록 배열
tags array 필수 태그 배열
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
type string 필수 블록 타입 ("text", "image", "video")
value string? 조건부 블록 내용 (text 타입일 때 필수, image/video 타입일 때 완전한 URL)
sequence number 필수 블록 순서
path string? 조건부 파일 상대 경로 (image/video 타입일 때만 제공)
thumbnail_value string? 조건부 비디오 썸네일 완전한 URL (video 타입일 때만 제공)
thumbnail_path string? 조건부 썸네일 상대 경로 (video 타입일 때만 제공)

주의사항:

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

3. 게시글 작성

POST /posts

새로운 게시글을 작성합니다.

Request Body

{
  "title": "게시글 제목",
  "sub_category_id": 1,
  "animal_type": "dog",
  "content_blocks": [
    {
      "type": "text",
      "value": "게시글 텍스트 내용입니다.",
      "sequence": 0
    },
    {
      "type": "image",
      "value": "http://localhost:3001/uploads/2024/01/15/image_123.jpg",
      "sequence": 1
    },
    {
      "type": "video",
      "value": "http://localhost:3001/uploads-post-video/2024/01/15/video_123.mp4",
      "thumbnail_path": "http://localhost:3001/uploads-post-video-thumbnail/2024/01/15/thumbnail_video_123.jpg",
      "sequence": 2
    }
  ],
  "tags": ["태그1", "태그2"]
}

Field Descriptions

필드 타입 필수 설명
title string 필수 게시글 제목 (1-150자)
sub_category_id number 필수 서브카테고리 ID
content_blocks array 필수 게시글 내용 블록 배열
content_blocks[].type string 필수 블록 타입 ("text", "image", "video")
content_blocks[].value string 조건부 블록 내용 (text 타입의 경우 필수)
content_blocks[].sequence number 필수 블록 순서 (0부터 시작)
content_blocks[].thumbnail_path string 조건부 비디오 썸네일 경로 (video 타입일 때만 제공)
tags array 선택 태그 배열 (최대 10개, 각 태그 최대 50자)

Content Block 처리 방법

📝 Text 블록:

  • value에 텍스트 내용을 직접 입력
  • HTML 태그는 지원하지 않음 (순수 텍스트만)

🖼️ Image 블록:

  • 1단계: POST /posts/upload/images API로 이미지 업로드
  • 2단계: 반환받은 이미지 URL을 value에 설정
  • 지원 형식: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif
  • 최대 파일 크기: 10MB

🎥 Video 블록:

  • 1단계: POST /posts/upload/video API로 영상 업로드
  • 2단계: 반환받은 영상 URL을 value에 설정
  • 3단계: 반환받은 썸네일 경로를 thumbnail_path에 설정
  • 지원 형식: .mp4, .avi, .mov, .wmv, .flv
  • 최대 파일 크기: 100MB
  • 단일 영상만 업로드 가능
  • 썸네일은 자동으로 생성되며, 영상의 50% 지점에서 추출

이미지/영상 업로드 플로우

1. 이미지/영상 파일 선택
2. POST /posts/upload/images 또는 /posts/upload/video 호출
3. 서버에서 파일 저장 후 URL 반환
4. 반환받은 URL을 content_blocks[].value에 설정
5. POST /posts로 게시글 작성

주의사항

  • 이미지/영상은 게시글 작성 전에 반드시 업로드해야 합니다
  • 업로드된 파일 URL은 게시글 작성 시 value 필드에 사용됩니다
  • 외부 이미지 URL은 보안상 지원하지 않습니다 (자체 업로드 파일만 사용 가능)

Response Example

{
  "code": 200,
  "message": "게시글이 작성되었습니다.",
  "data": {
    "postId": 1
  }
}

3-1. 게시글 작성 (통합 파일 업로드) - 신규

POST /posts/create-with-files

게시글의 텍스트, 이미지, 비디오를 한 번에 업로드하여 게시글을 작성합니다. 고아 파일 문제 해결을 위해 권장되는 API입니다.

Request

  • Method: POST
  • URL: /posts/create-with-files
  • Content-Type: multipart/form-data
  • 인증: JWT Bearer Token 필수 (Authorization 헤더)

Headers

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

FormData Fields

필드 타입 필수 설명
title string 필수 게시글 제목 (1-150자)
sub_category_id number 필수 서브카테고리 ID
content_blocks string 필수 콘텐츠 블록 배열의 JSON 문자열
images[] File[] 선택 이미지 파일들 (다중 가능, 필드명: images)
videos[] File[] 선택 비디오 파일들 (다중 가능, 필드명: videos)
tags string 선택 태그 배열의 JSON 문자열

Content Blocks JSON 구조

[
  {
    "type": "text",
    "value": "첫 번째 텍스트 블록 내용",
    "sequence": 1
  },
  {
    "type": "image",
    "value": null,
    "sequence": 2
  },
  {
    "type": "text",
    "value": "두 번째 텍스트 블록 내용",
    "sequence": 3
  },
  {
    "type": "video",
    "value": null,
    "editInfo": {
      "trimStart": 1000,
      "trimEnd": 5000,
      "cropArea": {
        "x": 0.1,
        "y": 0.1,
        "width": 0.8,
        "height": 0.8
      }
    },
    "sequence": 4
  }
]

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 createPostWithFiles = async (postData) => {
  const formData = new FormData();

  // 텍스트 데이터 추가
  formData.append('title', postData.title);
  formData.append('sub_category_id', postData.sub_category_id.toString());
  formData.append('content_blocks', JSON.stringify(postData.contentBlocks));
  if (postData.tags) {
    formData.append('tags', JSON.stringify(postData.tags));
  }

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

  // 비디오 파일들 추가
  postData.videos.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('/posts/create-with-files', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      // Content-Type은 자동으로 설정됨
    },
    body: formData,
  });

  return response.json();
};

// 사용 예시
const result = await createPostWithFiles({
  title: "나의 여행기",
  sub_category_id: 1,
  contentBlocks: [
    { type: 'text', value: '제주도 여행을 다녀왔어요!', sequence: 1 },
    { type: 'image', value: null, sequence: 2 },
    { type: 'text', value: '바다도 너무 예뻤어요.', sequence: 3 },
    { type: 'video', value: null, sequence: 4 }
  ],
  images: [imageFile],
  videos: [videoFile],
  tags: ['여행', '제주도']
});

🌐 웹 (React):

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

  return response.json();
};

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

  // 텍스트 데이터
  formData.append('title', title);
  formData.append('sub_category_id', subCategoryId);
  formData.append('content_blocks', JSON.stringify(contentBlocks));
  formData.append('tags', JSON.stringify(tags));

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

  const result = await createPostWithFiles(formData);
};

처리 플로우

  1. FormData 파싱: 텍스트 데이터와 파일들을 분리
  2. 콘텐츠 블록 매핑: content_blocks JSON과 파일들을 순서대로 매핑
  3. 파일 업로드: 이미지와 비디오들을 스토리지에 저장
  4. 블록 생성: 업로드된 파일 URL로 콘텐츠 블록들 생성
  5. 게시글 생성: 모든 데이터를 한 번에 DB에 저장 (트랜잭션)

특징

  • 고아 파일 방지: 파일 업로드와 게시글 생성이 원자적으로 처리
  • 블록 순서 보장: sequence 필드로 정확한 블록 순서 유지
  • 트랜잭션 보장: 파일 업로드 실패 시 게시글 생성 취소
  • 자동 정리: 실패 시 업로드된 파일들 자동 삭제

Response Example

{
  "code": 201,
  "message": "게시글이 작성되었습니다.",
  "data": {
    "postId": 123
  }
}

Error Handling

  • 파일 업로드 실패: 부분적으로 업로드된 파일들 자동 정리
  • 블록 매핑 오류: content_blocks JSON 형식 검증
  • 용량 초과: 개별 파일 크기 및 총 용량 제한

3-2. 게시글 수정 (통합 파일 업로드) - 신규

PUT /posts/{postId}/update-with-files

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

Request

  • Content-Type: multipart/form-data
  • 인증: JWT Bearer Token 필수
  • Path Parameters:
    • postId (number, 필수): 수정할 게시글 ID

FormData Fields

필드 타입 필수 설명
title string 필수 수정할 게시글 제목 (1-150자)
sub_category_id number 필수 수정할 서브카테고리 ID
content_blocks string 필수 콘텐츠 블록 JSON 문자열
images[] File[] 선택 새 이미지 파일들 (다중 가능)
videos[] File[] 선택 새 비디오 파일들 (다중 가능)
tags string 선택 태그 배열 JSON 문자열

Content Blocks JSON 구조

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

처리 플로우

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

특징

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

Response Example

{
  "code": 200,
  "message": "게시글이 수정되었습니다.",
  "data": {
    "postId": 123
  }
}

Error Handling

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

4. 게시글 수정

PUT /posts/{postId}

기존 게시글을 수정합니다. 본인이 작성한 게시글만 수정 가능합니다.

Path Parameters

파라미터 타입 필수 설명
postId number 필수 게시글 ID

Request Body

게시글 작성과 동일한 형식이지만 모든 필드가 선택사항입니다. 제공된 필드만 업데이트됩니다.

{
  "title": "수정된 게시글 제목",
  "content_blocks": [
    {
      "type": "text",
      "value": "수정된 내용입니다.",
      "sequence": 0
    },
    {
      "type": "image",
      "value": "http://localhost:3001/uploads/2024/01/15/updated_image.jpg",
      "sequence": 1
    },
    {
      "type": "video",
      "value": "http://localhost:3001/uploads-post-video/2024/01/15/updated_video.mp4",
      "thumbnail_path": "http://localhost:3001/uploads-post-video-thumbnail/2024/01/15/thumbnail_updated_video.jpg",
      "sequence": 2
    }
  ]
}

Response Example

{
  "code": 200,
  "message": "게시글이 수정되었습니다.",
  "data": {
    "id": 1,
    "updated_at": "2024-01-15T15:30:00.000Z"
  }
}

Response Fields

Root Data Object:

Field Type Required Description
id number 필수 게시글 고유 ID
updated_at string 필수 수정 시간 (ISO 8601)

5. 게시글 삭제

DELETE /posts/{postId}

게시글을 삭제합니다. 본인이 작성한 게시글만 삭제 가능합니다.

Path Parameters

파라미터 타입 필수 설명
postId number 필수 게시글 ID

Response Example

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

게시글 좋아요/북마크 API

6. 게시글 좋아요 토글 (권장)

POST /posts/{postId}/like

게시글의 좋아요 상태를 토글합니다. 좋아요가 되어있으면 취소하고, 되어있지 않으면 추가합니다.

요청

  • Method: POST
  • URL: /posts/{postId}/like
  • Headers:
    • Authorization: Bearer {token} (필수)
  • Path Parameters:
    • postId (number, 필수): 게시글 ID

응답

성공 (200):

{
  "code": 200,
  "message": "게시글을 좋아요했습니다.", // 또는 "게시글 좋아요를 취소했습니다."
  "data": {
    "is_liked": true, // 현재 좋아요 상태
    "like_count": 15  // 업데이트된 좋아요 수
  }
}

오류 응답:

  • 400: 잘못된 요청
  • 401: 인증 실패
  • 404: 게시글을 찾을 수 없음
  • 500: 서버 오류

게시글 북마크 목록 조회

GET /posts/bookmarks

사용자가 북마크한 게시글 목록을 조회합니다. 이미지 표시용으로 최적화되어 있으며 썸네일 URL만 반환합니다.

쿼리 파라미터

파라미터 타입 필수 설명
cursor string 선택 커서 기반 페이지네이션 (북마크 생성일 ISOString)
limit number 선택 한 번에 가져올 북마크 수 (기본값: 20, 최대: 50)

Request Example

GET /posts/bookmarks?cursor=2024-01-15T10:30:00.000Z&limit=25

Response Example

{
  "code": 200,
  "message": "북마크한 게시글 목록을 조회했습니다.",
  "data": {
    "items": [
      {
        "id": 123,
        "title": "북마크한 게시글 제목",
        "animal_type": "dog",
        "created_at": "2024-01-15T10:30:00.000Z",
        "created_at_bookmark": "2024-01-20T14:15:00.000Z",
        "user": {
          "id": 456,
          "nickname": "작성자명",
          "profile_img": "http://localhost:3001/uploads/profile.jpg"
        },
        "sub_category": {
          "id": 3,
          "name": "질문답변",
          "category": {
            "id": 1,
            "name": "개발"
          }
        },
        "like_count": 8,
        "bookmark_count": 3,
        "comment_count": 12,
        "preview_image": "http://localhost:3001/uploads-post-video-thumbnail/2024/01/15/thumbnail_video_123.jpg",
        "is_liked": true,
        "is_bookmarked": false
      },
      {
        "id": 124,
        "title": "또 다른 북마크 게시글",
        "created_at": "2024-01-14T15:20:00.000Z",
        "created_at_bookmark": "2024-01-19T09:30:00.000Z",
        "user": {
          "id": 789,
          "nickname": "다른작성자",
          "profile_img": null
        },
        "sub_category": {
          "id": 5,
          "name": "프로젝트",
          "category": {
            "id": 2,
            "name": "경험공유"
          }
        },
        "like_count": 15,
        "bookmark_count": 7,
        "comment_count": 5,
        "preview_image": "http://localhost:3001/uploads/2024/01/16/image_789.jpg"
      }
    ],
    "next_cursor": "2024-01-19T09:30:00.000Z"
  }
}

Response Fields

Root Data Object:

Field Type Required Description
items array 필수 북마크 게시글 목록
next_cursor string? 선택 다음 페이지 커서 (없음 = 마지막 페이지)

Bookmark Item Object (게시글 목록 조회 Response와 동일 구조):

Field Type Required Description
id number 필수 게시글 고유 ID
title string 필수 게시글 제목
created_at string 필수 게시글 작성 시간 (ISO 8601)
created_at_bookmark string 필수 북마크 추가 시간 (ISO 8601)
user object 필수 작성자 정보
sub_category object 필수 서브카테고리 정보
like_count number 필수 좋아요 수
bookmark_count number 필수 북마크 수
comment_count number 필수 댓글 수
preview_image string? 선택 미리보기 이미지 URL (첫 번째 이미지 또는 비디오 썸네일)
is_liked boolean? 선택 사용자 좋아요 여부 (로그인 시에만 제공)
is_bookmarked boolean? 선택 사용자 북마크 여부 (로그인 시에만 제공)

User Object:

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

SubCategory Object:

Field Type Required Description
id number 필수 서브카테고리 고유 ID
name string 필수 서브카테고리 이름
category object 필수 상위 카테고리 정보

주의사항:

  • 북마크된 게시글 목록은 기존 게시글 목록 조회와 동일한 상세 정보를 제공
  • created_at_bookmark는 북마크를 추가한 시점을 나타냅니다
  • 북마크 목록은 북마크 추가 시간 역순으로 정렬됩니다
  • 북마크된 모든 게시물에 대해서 상세한故事을 포함한 전체 정보를 반환합니다

Query Optimization

  • N+1 문제 해결: 단일 JOIN 쿼리로 북마크 데이터와 게시글 기본 정보를 함께 조회
  • 최소 데이터 전송: 대표 이미지 URL만 포함하여 전송 비용 절감
  • 인덱스 활용: 북마크 생성일(created_at) 기준으로 최적화된 페이지네이션

Response Field Details

preview_image 처리 우선순위:

  1. 게시글에 포함된 첫 번째 이미지를 대표 이미지로 사용
  2. 이미지가 없으면 비디오 썸네일을 대표 이미지로 사용
  3. 아무것도 없으면 null 반환

주의사항:

  • 로그인이 반드시 필요합니다 (auth 미들웨어)
  • 북마크는 최신순(생성일 기준 내림차순)으로 정렬됩니다
  • 이미지 표시용으로 최적화되어 있어 다른 정보는 포함하지 않습니다

게시글 북마크 토글 (권장)

POST /posts/{postId}/bookmark

게시글의 북마크 상태를 토글합니다. 북마크가 되어있으면 취소하고, 되어있지 않으면 추가합니다.

요청

  • Method: POST
  • URL: /posts/{postId}/bookmark
  • Headers:
    • Authorization: Bearer {token} (필수)
  • Path Parameters:
    • postId (number, 필수): 게시글 ID

응답

성공 (200):

{
  "code": 200,
  "message": "게시글을 북마크했습니다.", // 또는 "게시글 북마크를 취소했습니다."
  "data": {
    "is_bookmarked": true, // 현재 북마크 상태
    "bookmark_count": 8    // 업데이트된 북마크 수
  }
}

오류 응답:

  • 400: 잘못된 요청
  • 401: 인증 실패
  • 404: 게시글을 찾을 수 없음
  • 500: 서버 오류

게시글 좋아요 (기존 API - 호환성 유지)

POST /posts/{postId}/likes

게시글에 좋아요를 추가합니다.

요청

  • Method: POST
  • URL: /posts/{postId}/likes
  • Headers:
    • Authorization: Bearer {token} (필수)
  • Path Parameters:
    • postId (number, 필수): 게시글 ID

응답

성공 (200):

{
  "code": 200,
  "message": "게시글을 좋아요했습니다.",
  "data": null
}

오류 응답:

  • 400: 잘못된 요청
  • 401: 인증 실패
  • 404: 게시글을 찾을 수 없음
  • 409: 이미 좋아요한 게시글
  • 500: 서버 오류

게시글 좋아요 취소 (기존 API - 호환성 유지)

DELETE /posts/{postId}/likes

게시글의 좋아요를 취소합니다.

요청

  • Method: DELETE
  • URL: /posts/{postId}/likes
  • Headers:
    • Authorization: Bearer {token} (필수)
  • Path Parameters:
    • postId (number, 필수): 게시글 ID

응답

성공 (200):

{
  "code": 200,
  "message": "게시글 좋아요를 취소했습니다.",
  "data": null
}

오류 응답:

  • 400: 잘못된 요청
  • 401: 인증 실패
  • 404: 게시글을 찾을 수 없음
  • 409: 좋아요하지 않은 게시글
  • 500: 서버 오류

게시글 북마크 (기존 API - 호환성 유지)

POST /posts/{postId}/bookmarks

게시글을 북마크에 추가합니다.

요청

  • Method: POST
  • URL: /posts/{postId}/bookmarks
  • Headers:
    • Authorization: Bearer {token} (필수)
  • Path Parameters:
    • postId (number, 필수): 게시글 ID

응답

성공 (200):

{
  "code": 200,
  "message": "게시글을 북마크했습니다.",
  "data": null
}

오류 응답:

  • 400: 잘못된 요청
  • 401: 인증 실패
  • 404: 게시글을 찾을 수 없음
  • 409: 이미 북마크한 게시글
  • 500: 서버 오류

게시글 북마크 취소 (기존 API - 호환성 유지)

DELETE /posts/{postId}/bookmarks

게시글의 북마크를 취소합니다.

요청

  • Method: DELETE
  • URL: /posts/{postId}/bookmarks
  • Headers:
    • Authorization: Bearer {token} (필수)
  • Path Parameters:
    • postId (number, 필수): 게시글 ID

응답

성공 (200):

{
  "code": 200,
  "message": "게시글 북마크를 취소했습니다.",
  "data": null
}

오류 응답:

  • 400: 잘못된 요청
  • 401: 인증 실패
  • 404: 게시글을 찾을 수 없음
  • 409: 북마크하지 않은 게시글
  • 500: 서버 오류

댓글 API

10. 댓글 목록 조회

GET /posts/{postId}/comments

특정 게시글의 댓글 목록을 조회합니다. 대댓글(답글)도 포함됩니다. 커서 기반 페이지네이션을 지원합니다.

Path Parameters

파라미터 타입 필수 설명
postId number 필수 게시글 ID

Query Parameters

파라미터 타입 필수 설명
cursor string 선택 페이지네이션 커서 (댓글 ID)
limit number 선택 가져올 댓글 수 (기본값: 20, 최대: 50)

Request Example

GET /posts/1/comments?cursor=100&limit=10

Response Example

{
  "code": 200,
  "message": "댓글 목록을 조회했습니다.",
  "data": {
    "items": [
      {
        "id": 101,
        "content": "좋은 게시글이네요!",
        "created_at": "2024-01-15T10:35:00.000Z",
        "updated_at": "2024-01-15T10:35:00.000Z",
        "user": {
          "id": 2,
          "nickname": "댓글작성자",
          "profile_img": "http://localhost:3001/uploads/profile2.jpg"
        },
        "parent_comment_id": null,
        "mention_user": null,
        "like_count": 2,
        "is_liked": false,
        "is_author": false,
        "is_deleted": false,
        "replies": [
          {
            "id": 102,
            "content": "@댓글작성자 감사합니다!",
            "created_at": "2024-01-15T10:40:00.000Z",
            "updated_at": "2024-01-15T10:40:00.000Z",
            "user": {
              "id": 1,
              "nickname": "게시글작성자",
              "profile_img": "http://localhost:3001/uploads/profile1.jpg"
            },
            "parent_comment_id": 101,
            "mention_user": {
              "id": 2,
              "nickname": "댓글작성자"
            },
            "like_count": 1,
            "is_liked": true,
            "is_author": true,
            "is_deleted": false
          }
        ]
      }
    ],
    "next_cursor": "90"
  }
}

응답 필드 설명 (삭제된 댓글의 경우)

  • content: 항상 "삭제된 댓글입니다."로 표시
  • user.id: 0으로 설정
  • user.nickname: "알 수 없음"으로 설정
  • user.profile_img: null로 설정
  • like_count: 0으로 설정
  • is_liked: 항상 false
  • is_author: 항상 false
  • is_deleted: true로 설정
  • mention_user: null로 설정 (언급 정보 숨김)
  • replies: 삭제된 댓글이어도 답글은 정상적으로 표시됨

#### Response Fields

**Comment Object**:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | number | 필수 | 댓글 고유 ID |
| content | string | 필수 | 댓글 내용 (삭제된 경우 '삭제된 댓글입니다.') |
| created_at | string | 필수 | 댓글 작성 시간 (ISO 8601) |
| updated_at | string | 필수 | 댓글 수정 시간 (ISO 8601) |
| user | object | 필수 | 작성자 정보 (삭제된 경우 id: 0, nickname: '알 수 없음', profile_img: null) |
| parent_comment_id | number? | 선택 | 부모 댓글 ID (대댓글인 경우) |
| mention_user | object? | 선택 | 멘션된 사용자 정보 (삭제된 경우 null) |
| like_count | number | 필수 | 댓글 좋아요 수 (삭제된 경우 0) |
| is_liked | boolean | 필수 | 사용자 좋아요 여부 (삭제된 경우 항상 false) |
| is_author | boolean | 필수 | 댓글 작성자 여부 (삭제된 경우 항상 false) |
| is_deleted | boolean | 필수 | 댓글 삭제 여부 |
| replies | array? | 선택 | 답글 배열 (최상위 댓글에만 포함, 삭제된 댓글의 경우 빈 배열) |

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

**주의사항**:
- `is_liked`: 로그인한 사용자에게만 제공됩니다. 삭제된 댓글의 경우 항상 false입니다.
- `is_author`: 로그인한 사용자에게만 제공되며, 자신이 작성한 댓글인지 여부를 나타냅니다. 삭제된 댓글의 경우 항상 false입니다.
- `is_deleted`: 댓글이 삭제되었는지 여부를 나타냅니다. 삭제된 경우 content는 '삭제된 댓글입니다.'로 표시되며, user 정보는 기본값으로 대체됩니다.
- `replies`: 최상위 댓글에만 포함되며, 답글에는 포함되지 않습니다. 삭제된 댓글의 경우 빈 배열이 반환됩니다.
- `parent_comment_id`: 최상위 댓글은 null, 답글은 부모 댓글 ID를 가집니다.
- 삭제된 댓글의 경우 like_count는 0으로, mention_user는 null로 설정됩니다.

---

### 11. 댓글 작성
**POST** `/posts/{postId}/comments`

게시글에 댓글 또는 대댓글을 작성합니다.

#### Path Parameters
| 파라미터 | 타입 | 필수 | 설명 |
|----------|------|------|------|
| postId | number | 필수 | 게시글 ID |

#### Request Body
```json
{
  "content": "댓글 내용입니다.",
  "parent_comment_id": 1,
  "mention_user_id": 2
}

Field Descriptions

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

Response Example

{
  "code": 200,
  "message": "댓글이 작성되었습니다.",
  "data": {
    "commentId": 1
  }
}

12. 댓글 수정

PUT /comments/{commentId}

댓글을 수정합니다. 본인이 작성한 댓글만 수정 가능합니다.

Path Parameters

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

Request Body

{
  "content": "수정된 댓글 내용입니다."
}

Response Example

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

13. 댓글 삭제 (소프트 딜리트)

PATCH /comments/{commentId}/status

댓글을 소프트 딜리트합니다. 본인이 작성한 댓글만 삭제 가능합니다. 실제로 데이터베이스에서 삭제되는 것이 아니라 is_deleted 플래그가 true로 설정됩니다.

Path Parameters

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

Request Body

{
  "is_deleted": true
}

Response Example

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

14. 댓글 좋아요 토글 (권장)

POST /comments/{commentId}/like

댓글의 좋아요 상태를 토글합니다. 좋아요가 되어있으면 취소하고, 되어있지 않으면 추가합니다.

Path Parameters

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

Response Example

{
  "code": 200,
  "message": "댓글을 좋아요했습니다.", // 또는 "댓글 좋아요를 취소했습니다."
  "data": {
    "is_liked": true, // 현재 좋아요 상태
    "like_count": 5   // 업데이트된 좋아요 수
  }
}

오류 응답:

  • 400: 잘못된 요청
  • 401: 인증 실패
  • 404: 댓글을 찾을 수 없음
  • 500: 서버 오류

15. 댓글 좋아요 (기존 API - 호환성 유지)

POST /comments/{commentId}/likes

댓글에 좋아요를 추가합니다.

Path Parameters

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

Response Example

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

16. 댓글 좋아요 취소 (기존 API - 호환성 유지)

DELETE /comments/{commentId}/likes

댓글 좋아요를 취소합니다.

Path Parameters

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

Response Example

{
  "code": 200,
  "message": "댓글 좋아요를 취소했습니다.",
  "data": null
}

댓글 API

게시글에 사용할 이미지를 업로드합니다.

Request

  • Content-Type: multipart/form-data
  • Field Name: images (다중 파일 업로드 가능)
  • 지원 형식: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif
  • 최대 파일 크기: 10MB
  • 최대 파일 개수: 10개

클라이언트 구현 가이드

📱 React Native / Expo:

import * as ImagePicker from 'expo-image-picker';

const uploadImage = async () => {
  try {
    // 1. 이미지 선택
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsMultipleSelection: true,
      quality: 1,
    });

    if (!result.canceled) {
      // 2. FormData 생성
      const formData = new FormData();
      
      result.assets.forEach((asset, index) => {
        const imageUri = asset.uri;
        const fileName = imageUri.split('/').pop() || `image_${index}.jpg`;
        
        formData.append('images', {
          uri: imageUri,
          type: 'image/jpeg', // 또는 asset.type
          name: fileName,
        } as any);
      });

      // 3. API 호출
      const response = await fetch('http://localhost:3001/posts/upload/images', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'multipart/form-data',
        },
        body: formData,
      });

      const data = await response.json();
      console.log('업로드된 이미지 URL:', data.data.files[0].url);
    }
  } catch (error) {
    console.error('이미지 업로드 실패:', error);
  }
};

🌐 웹 (React):

const uploadImage = async (files) => {
  try {
    const formData = new FormData();
    
    // FileList를 FormData에 추가
    Array.from(files).forEach((file) => {
      formData.append('images', file);
    });

    const response = await fetch('/posts/upload/images', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        // Content-Type은 자동으로 설정됨
      },
      body: formData,
    });

    const data = await response.json();
    return data.data.files;
  } catch (error) {
    console.error('이미지 업로드 실패:', error);
  }
};

// 사용 예시
const handleFileChange = (event) => {
  const files = event.target.files;
  uploadImage(files);
};

FormData 구조 상세

📋 FormData 필드 구조:

FormData {
  images: File | Blob | {
    uri: string,        // React Native에서 파일 경로
    type: string,       // MIME 타입 (image/jpeg, image/png 등)
    name: string        // 파일명 (확장자 포함)
  }
}

🔑 핵심 포인트:

  • Field Name: 반드시 images여야 함
  • 파일 객체: 실제 이미지 파일 데이터
  • MIME 타입: 서버에서 파일 형식 검증에 사용
  • 파일명: 확장자 포함하여 서버에서 파일 타입 확인

지원 이미지 형식 상세

형식 MIME 타입 설명 용도
JPG/JPEG image/jpeg 가장 흔한 이미지 형식 일반 사진, 웹 이미지
PNG image/png 투명도 지원 스크린샷, 로고, 아이콘
GIF image/gif 애니메이션 지원 움직이는 이미지
WebP image/webp 구글 개발 최신 형식 웹 최적화 이미지
HEIC/HEIF image/heic, image/heif 최신 아이폰 기본 형식 고품질 사진, 최신 모바일

Response Example

✅ 성공 응답:

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

❌ 에러 응답 예시:

파일 형식 오류:

{
  "code": 400,
  "message": "허용되지 않는 파일 형식입니다. 허용된 확장자: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif",
  "errors": []
}

파일 크기 초과:

{
  "code": 400,
  "message": "파일 크기가 너무 큽니다. 최대 10MB까지 허용됩니다.",
  "errors": []
}

파일 개수 초과:

{
  "code": 400,
  "message": "업로드할 수 있는 파일 개수를 초과했습니다. 최대 10개까지 허용됩니다.",
  "errors": []
}

응답 필드 설명

필드 타입 설명
files[].filename string 서버에서 생성한 고유 파일명
files[].path string 서버 내부 저장 경로
files[].url string 클라이언트에서 사용할 이미지 URL
files[].size number 파일 크기 (바이트)

클라이언트 사용법

업로드된 이미지 URL 사용:

// 1. 이미지 업로드
const uploadResult = await uploadImage(files);
const imageUrl = uploadResult.files[0].url;

// 2. 게시글 작성 시 사용
const postData = {
  title: "게시글 제목",
  sub_category_id: 1,
  content_blocks: [
    {
      type: "text",
      value: "게시글 내용",
      sequence: 0
    },
    {
      type: "image",
      value: imageUrl, // 업로드된 이미지 URL
      sequence: 1
    }
  ],
  tags: ["태그1"]
};

// 3. 게시글 작성 API 호출
await createPost(postData);

오류 코드

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

2. 영상 업로드

POST /posts/upload/video

게시글에 첨부할 영상을 업로드합니다. 단일 영상만 업로드 가능합니다.

Headers

헤더 타입 필수 설명
Authorization string 필수 Bearer 토큰
Content-Type string 필수 multipart/form-data

Request Body

필드 타입 필수 설명
video File 필수 업로드할 영상 파일 (단일)

지원 영상 형식

형식 MIME 타입 설명 용도
MP4 video/mp4 가장 흔한 영상 형식 일반 영상, 웹 영상
AVI video/x-msvideo 윈도우 기본 형식 고품질 영상
MOV video/quicktime 애플 기본 형식 아이폰 촬영 영상
WMV video/x-ms-wmv 윈도우 미디어 형식 윈도우 영상
FLV video/x-flv 플래시 영상 형식 웹 스트리밍

제한사항

  • 최대 파일 크기: 100MB
  • 업로드 개수: 1개 (단일 영상만)
  • 영상 길이: 1분 이내 권장 (클라이언트에서 제한)
  • 필드명: 반드시 video여야 함

사용 예시

📱 React Native:

import { launchImageLibrary } from 'react-native-image-picker';

const uploadVideo = async () => {
  try {
    const result = await launchImageLibrary({
      mediaType: 'video',
      quality: 0.8,
    });

    if (result.assets && result.assets[0]) {
      const video = result.assets[0];
      
      const formData = new FormData();
      formData.append('video', {
        uri: video.uri,
        type: video.type,
        name: video.fileName || 'video.mp4',
      });

      const response = await fetch('/posts/upload/video', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'multipart/form-data',
        },
        body: formData,
      });

      const data = await response.json();
      if (data.code === 200) {
        console.log('업로드된 영상 URL:', data.data.file.url);
      }
    }
  } catch (error) {
    console.error('영상 업로드 실패:', error);
  }
};

🌐 웹 (React):

const uploadVideo = async (file) => {
  try {
    const formData = new FormData();
    formData.append('video', file);

    const response = await fetch('/posts/upload/video', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        // Content-Type은 자동으로 설정됨
      },
      body: formData,
    });

    const data = await response.json();
    return data.data.file;
  } catch (error) {
    console.error('영상 업로드 실패:', error);
  }
};

// 사용 예시
const handleVideoChange = (event) => {
  const file = event.target.files[0];
  if (file) {
    uploadVideo(file);
  }
};

Response Example

✅ 성공 응답 (썸네일 생성 성공):

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

✅ 성공 응답 (썸네일 생성 실패 - 영상만 업로드):

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

❌ 에러 응답 예시:

파일 형식 오류:

{
  "code": 400,
  "message": "허용되지 않는 파일 형식입니다. 허용된 확장자: .mp4, .avi, .mov, .wmv, .flv",
  "errors": []
}

파일 크기 초과:

{
  "code": 400,
  "message": "파일 크기가 너무 큽니다. 최대 100MB까지 업로드 가능합니다.",
  "errors": []
}

파일 없음:

{
  "code": 400,
  "message": "업로드할 영상을 선택해주세요.",
  "errors": []
}

응답 필드 설명

필드 타입 설명
file.filename string 서버에서 생성한 고유 파일명
file.path string 서버 내부 저장 경로
file.url string 클라이언트에서 사용할 영상 URL
file.size number 파일 크기 (바이트)

클라이언트 사용법

업로드된 영상 URL 사용:

// 1. 영상 업로드
const uploadResult = await uploadVideo(file);
const videoUrl = uploadResult.file.url;

// 2. 게시글 작성 시 사용
const postData = {
  title: "게시글 제목",
  sub_category_id: 1,
  content_blocks: [
    {
      type: "text",
      value: "게시글 내용",
      sequence: 0
    },
    {
      type: "video",
      value: videoUrl, // 업로드된 영상 URL
      thumbnail_path: thumbnailUrl, // 업로드된 썸네일 URL
      sequence: 1
    }
  ],
  tags: ["영상", "게시글"]
};

// 3. 게시글 작성 API 호출
const response = await fetch('/posts', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(postData),
});

인증

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

헤더 방식 (모바일 앱)

Authorization: Bearer YOUR_JWT_TOKEN

쿠키 방식 (웹)

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

게시글 타입

모든 게시글은 텍스트, 이미지, 비디오 블록으로 구성된 일반 게시글입니다.

  • 다양한 콘텐츠 블록을 조합하여 풍부한 게시글 작성 가능
  • 태그를 통한 게시글 분류 및 검색 지원

파일 업로드 정책

저장 구조

이미지 저장소:

uploads/
├── 2024/
│   ├── 01/
│   │   ├── 15/
│   │   │   ├── image_1640000000000_abc123.jpg
│   │   │   └── image_1640000000001_def456.png
│   │   └── 16/
│   └── 02/
└── 2025/

영상 저장소:

uploads-post-video/
├── 2024/
│   ├── 01/
│   │   ├── 15/
│   │   │   ├── video_1640000000000_abc123.mp4
│   │   │   └── video_1640000000001_def456.mov
│   │   └── 16/
│   └── 02/
└── 2025/

썸네일 저장소:

uploads-post-video-thumbnail/
├── 2024/
│   ├── 01/
│   │   ├── 15/
│   │   │   ├── thumbnail_video_1640000000000_abc123.jpg
│   │   │   └── thumbnail_video_1640000000001_def456.jpg
│   │   └── 16/
│   └── 02/
└── 2025/

파일명 규칙

  • 원본파일명_타임스탬프_랜덤문자열.확장자
  • 예: profile_1640000000000_abc123.jpg

접근 URL

  • 이미지: /uploads 경로를 통해 정적 파일로 서빙됩니다.
    • 예: http://localhost:3001/uploads/2024/01/15/image_1640000000000_abc123.jpg
  • 영상: /uploads-post-video 경로를 통해 정적 파일로 서빙됩니다.
    • 예: http://localhost:3001/uploads-post-video/2024/01/15/video_1640000000000_abc123.mp4
  • 썸네일: /uploads-post-video-thumbnail 경로를 통해 정적 파일로 서빙됩니다.
    • 예: http://localhost:3001/uploads-post-video-thumbnail/2024/01/15/thumbnail_video_1640000000000_abc123.jpg

성능 최적화

데이터베이스 인덱스

  • Post.id (Primary Key)
  • Post.user_id + Post.created_at (DESC)
  • Post.sub_category_id + Post.created_at
  • PostContent.post_id + PostContent.sequence
  • PostLike.post_id + PostLike.user_id
  • PostBookmark.post_id + PostBookmark.user_id
  • PostTag.post_id + PostTag.tag_id
  • Tag.name (Full-text search용)

캐싱 전략

  • 게시글 목록: 1분 캐시 (Redis)
  • 게시글 상세: 5분 캐시 (Redis)
  • 좋아요/북마크 상태: 10분 캐시 (Redis)
  • 태그 목록: 1시간 캐시 (Redis)

검색 최적화

  • Full-text search 인덱스 (제목, 내용)
  • 태그 기반 빠른 필터링
  • 사용자별 맞춤 추천 알고리즘 (향후 구현)

페이지네이션 최적화

  • 커서 기반 페이지네이션 (created_at 기준)
  • 인덱스를 활용한 빠른 조회
  • 최대 50개까지 한 번에 조회 가능

이미지 최적화

  • WebP 포맷 우선 사용
  • 썸네일 자동 생성 (100x100, 300x300)
  • CDN 연동 (향후 구현)

3. 동물 타입 목록 조회

GET /posts/animal-types

서버에서 관리하는 동물 타입 목록을 조회합니다. 클라이언트는 이 API를 통해 드롭다운이나 선택 UI를 동적으로 구성할 수 있습니다.

Request Example

GET /posts/animal-types

Response Example

{
  "code": 200,
  "message": "동물 타입 목록을 조회했습니다.",
  "data": {
    "animal_types": [
      {
        "value": "dog",
        "label": "강아지"
      },
      {
        "value": "cat",
        "label": "고양이"
      },
      {
        "value": "small_pet",
        "label": "소동물"
      },
      {
        "value": "bird",
        "label": ""
      },
      {
        "value": "reptile",
        "label": "파충류"
      },
      {
        "value": "fish",
        "label": "물고기"
      },
      {
        "value": "other",
        "label": "기타"
      }
    ]
  }
}

Response Fields

Field Type Required Description
animal_types array 필수 동물 타입 목록
animal_types[].value string 필수 동물 타입 값 (enum 값)
animal_types[].label string 필수 동물 타입 표시명 (UI 표시용)

특징

  • 서버 관리: 동물 타입을 서버에서 중앙 집중 관리
  • 클라이언트 하드코딩 제거: 클라이언트가 enum 값을 직접 정의하지 않아도 됨
  • 확장성: 새로운 동물 타입 추가 시 클라이언트 변경 불필요
  • 다국어 지원: label을 현지화하여 다국어 지원 가능

4. 게시글 영상 편집 업로드

POST /posts/upload/video/edit

업로드된 영상을 trim과 crop 파라미터로 편집하고, 편집된 영상과 썸네일을 생성합니다.

상세한 API 명세는 피드 영상 편집 업로드 API(/feeds/upload/video/edit)를 참고해주세요. 동일한 방식으로 동작합니다.