게시글, 댓글, 좋아요, 북마크 관련 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": "상세 오류 메시지"
}
]
}GET /posts
게시글 목록을 페이지네이션으로 조회합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| categoryId | number | 선택 | 카테고리 ID |
| subCategoryId | number | 선택 | 서브카테고리 ID |
| animalType | string | 선택 | 동물 타입 ('dog', 'cat', 'small_pet', 'bird', 'reptile', 'fish', 'other') |
| page | number | 선택 | 페이지 번호 (기본값: 1) |
GET /posts?categoryId=1&page=1
GET /posts?subCategoryId=3&page=2
{
"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
}
}
}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: 로그인한 사용자에게만 제공됩니다.
GET /posts/{postId}
특정 게시글의 상세 정보를 조회합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| postId | number | 필수 | 게시글 ID |
GET /posts/1
{
"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
}
}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일 수 있습니다.
POST /posts
새로운 게시글을 작성합니다.
{
"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"]
}| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| 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자) |
📝 Text 블록:
value에 텍스트 내용을 직접 입력- HTML 태그는 지원하지 않음 (순수 텍스트만)
🖼️ Image 블록:
- 1단계:
POST /posts/upload/imagesAPI로 이미지 업로드 - 2단계: 반환받은 이미지 URL을
value에 설정 - 지원 형식: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif
- 최대 파일 크기: 10MB
🎥 Video 블록:
- 1단계:
POST /posts/upload/videoAPI로 영상 업로드 - 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은 보안상 지원하지 않습니다 (자체 업로드 파일만 사용 가능)
{
"code": 200,
"message": "게시글이 작성되었습니다.",
"data": {
"postId": 1
}
}POST /posts/create-with-files
게시글의 텍스트, 이미지, 비디오를 한 번에 업로드하여 게시글을 작성합니다. 고아 파일 문제 해결을 위해 권장되는 API입니다.
- Method:
POST - URL:
/posts/create-with-files - Content-Type:
multipart/form-data - 인증: JWT Bearer Token 필수 (Authorization 헤더)
| 헤더명 | 타입 | 필수 | 설명 |
|---|---|---|---|
Idempotency-Key |
String | 필수 | 중복 요청 방지용 UUID v4 |
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
title |
string | 필수 | 게시글 제목 (1-150자) |
sub_category_id |
number | 필수 | 서브카테고리 ID |
content_blocks |
string | 필수 | 콘텐츠 블록 배열의 JSON 문자열 |
images[] |
File[] | 선택 | 이미지 파일들 (다중 가능, 필드명: images) |
videos[] |
File[] | 선택 | 비디오 파일들 (다중 가능, 필드명: videos) |
tags |
string | 선택 | 태그 배열의 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
}
]비디오 블록에 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);
};- FormData 파싱: 텍스트 데이터와 파일들을 분리
- 콘텐츠 블록 매핑:
content_blocksJSON과 파일들을 순서대로 매핑 - 파일 업로드: 이미지와 비디오들을 스토리지에 저장
- 블록 생성: 업로드된 파일 URL로 콘텐츠 블록들 생성
- 게시글 생성: 모든 데이터를 한 번에 DB에 저장 (트랜잭션)
- 고아 파일 방지: 파일 업로드와 게시글 생성이 원자적으로 처리
- 블록 순서 보장:
sequence필드로 정확한 블록 순서 유지 - 트랜잭션 보장: 파일 업로드 실패 시 게시글 생성 취소
- 자동 정리: 실패 시 업로드된 파일들 자동 삭제
{
"code": 201,
"message": "게시글이 작성되었습니다.",
"data": {
"postId": 123
}
}- 파일 업로드 실패: 부분적으로 업로드된 파일들 자동 정리
- 블록 매핑 오류:
content_blocksJSON 형식 검증 - 용량 초과: 개별 파일 크기 및 총 용량 제한
PUT /posts/{postId}/update-with-files
게시글의 텍스트, 이미지, 비디오를 한 번에 수정합니다. 기존 파일들은 자동으로 정리됩니다.
- Content-Type:
multipart/form-data - 인증: JWT Bearer Token 필수
- Path Parameters:
postId(number, 필수): 수정할 게시글 ID
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| title | string | 필수 | 수정할 게시글 제목 (1-150자) |
| sub_category_id | number | 필수 | 수정할 서브카테고리 ID |
| content_blocks | string | 필수 | 콘텐츠 블록 JSON 문자열 |
| images[] | File[] | 선택 | 새 이미지 파일들 (다중 가능) |
| videos[] | File[] | 선택 | 새 비디오 파일들 (다중 가능) |
| tags | string | 선택 | 태그 배열 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
}
]- 기존 콘텐츠 블록 조회 → 현재 파일들 파악
- 새 데이터 파싱 → FormData에서 텍스트/파일 분리
- 콘텐츠 블록 재구성 → 새 파일 업로드 + 기존 파일 유지
- 파일 비교 → 사용하지 않는 기존 파일들 식별
- DB 업데이트 → 트랜잭션으로 안전하게 저장
- 파일 정리 → 사용하지 않는 파일들 자동 삭제
- 자동 파일 정리: 기존 파일들 중 사용하지 않는 것들은 자동 삭제
- 트랜잭션 보장: DB 업데이트 실패 시 파일 정리 취소
- 비디오 편집 지원: 수정 시에도 trim + crop 적용 가능
- URL 기반 비교: sequence와 무관하게 파일 존재 여부로 정리
{
"code": 200,
"message": "게시글이 수정되었습니다.",
"data": {
"postId": 123
}
}- 권한 없음: 본인이 작성한 게시글만 수정 가능
- 파일 정리 실패: DB는 업데이트되지만 파일 삭제 실패 시 로그 기록
- 트랜잭션 롤백: 업데이트 중 오류 발생 시 모든 변경사항 취소
PUT /posts/{postId}
기존 게시글을 수정합니다. 본인이 작성한 게시글만 수정 가능합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| postId | number | 필수 | 게시글 ID |
게시글 작성과 동일한 형식이지만 모든 필드가 선택사항입니다. 제공된 필드만 업데이트됩니다.
{
"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
}
]
}{
"code": 200,
"message": "게시글이 수정되었습니다.",
"data": {
"id": 1,
"updated_at": "2024-01-15T15:30:00.000Z"
}
}Root Data Object:
| Field | Type | Required | Description |
|---|---|---|---|
| id | number | 필수 | 게시글 고유 ID |
| updated_at | string | 필수 | 수정 시간 (ISO 8601) |
DELETE /posts/{postId}
게시글을 삭제합니다. 본인이 작성한 게시글만 삭제 가능합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| postId | number | 필수 | 게시글 ID |
{
"code": 200,
"message": "게시글이 삭제되었습니다.",
"data": null
}게시글의 좋아요 상태를 토글합니다. 좋아요가 되어있으면 취소하고, 되어있지 않으면 추가합니다.
요청
- 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: 서버 오류
사용자가 북마크한 게시글 목록을 조회합니다. 이미지 표시용으로 최적화되어 있으며 썸네일 URL만 반환합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| cursor | string | 선택 | 커서 기반 페이지네이션 (북마크 생성일 ISOString) |
| limit | number | 선택 | 한 번에 가져올 북마크 수 (기본값: 20, 최대: 50) |
GET /posts/bookmarks?cursor=2024-01-15T10:30:00.000Z&limit=25
{
"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"
}
}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는 북마크를 추가한 시점을 나타냅니다- 북마크 목록은 북마크 추가 시간 역순으로 정렬됩니다
- 북마크된 모든 게시물에 대해서 상세한故事을 포함한 전체 정보를 반환합니다
- N+1 문제 해결: 단일 JOIN 쿼리로 북마크 데이터와 게시글 기본 정보를 함께 조회
- 최소 데이터 전송: 대표 이미지 URL만 포함하여 전송 비용 절감
- 인덱스 활용: 북마크 생성일(created_at) 기준으로 최적화된 페이지네이션
preview_image 처리 우선순위:
- 게시글에 포함된 첫 번째 이미지를 대표 이미지로 사용
- 이미지가 없으면 비디오 썸네일을 대표 이미지로 사용
- 아무것도 없으면
null반환
주의사항:
- 로그인이 반드시 필요합니다 (
auth 미들웨어) - 북마크는 최신순(생성일 기준 내림차순)으로 정렬됩니다
- 이미지 표시용으로 최적화되어 있어 다른 정보는 포함하지 않습니다
게시글의 북마크 상태를 토글합니다. 북마크가 되어있으면 취소하고, 되어있지 않으면 추가합니다.
요청
- 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: 서버 오류
게시글에 좋아요를 추가합니다.
요청
- 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: 서버 오류
게시글의 좋아요를 취소합니다.
요청
- 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: 서버 오류
게시글을 북마크에 추가합니다.
요청
- 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: 서버 오류
게시글의 북마크를 취소합니다.
요청
- 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: 서버 오류
GET /posts/{postId}/comments
특정 게시글의 댓글 목록을 조회합니다. 대댓글(답글)도 포함됩니다. 커서 기반 페이지네이션을 지원합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| postId | number | 필수 | 게시글 ID |
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| cursor | string | 선택 | 페이지네이션 커서 (댓글 ID) |
| limit | number | 선택 | 가져올 댓글 수 (기본값: 20, 최대: 50) |
GET /posts/1/comments?cursor=100&limit=10
{
"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: 항상 falseis_author: 항상 falseis_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
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| content | string | 필수 | 댓글 내용 (1-1000자) |
| parent_comment_id | number | 선택 | 부모 댓글 ID (대댓글인 경우) |
| mention_user_id | number | 선택 | 멘션할 사용자 ID |
{
"code": 200,
"message": "댓글이 작성되었습니다.",
"data": {
"commentId": 1
}
}PUT /comments/{commentId}
댓글을 수정합니다. 본인이 작성한 댓글만 수정 가능합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| commentId | number | 필수 | 댓글 ID |
{
"content": "수정된 댓글 내용입니다."
}{
"code": 200,
"message": "댓글이 수정되었습니다.",
"data": null
}PATCH /comments/{commentId}/status
댓글을 소프트 딜리트합니다. 본인이 작성한 댓글만 삭제 가능합니다.
실제로 데이터베이스에서 삭제되는 것이 아니라 is_deleted 플래그가 true로 설정됩니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| commentId | number | 필수 | 댓글 ID |
{
"is_deleted": true
}{
"code": 200,
"message": "댓글이 삭제 처리되었습니다.",
"data": null
}POST /comments/{commentId}/like
댓글의 좋아요 상태를 토글합니다. 좋아요가 되어있으면 취소하고, 되어있지 않으면 추가합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| commentId | number | 필수 | 댓글 ID |
{
"code": 200,
"message": "댓글을 좋아요했습니다.", // 또는 "댓글 좋아요를 취소했습니다."
"data": {
"is_liked": true, // 현재 좋아요 상태
"like_count": 5 // 업데이트된 좋아요 수
}
}오류 응답:
- 400: 잘못된 요청
- 401: 인증 실패
- 404: 댓글을 찾을 수 없음
- 500: 서버 오류
POST /comments/{commentId}/likes
댓글에 좋아요를 추가합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| commentId | number | 필수 | 댓글 ID |
{
"code": 200,
"message": "댓글을 좋아요했습니다.",
"data": null
}DELETE /comments/{commentId}/likes
댓글 좋아요를 취소합니다.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| commentId | number | 필수 | 댓글 ID |
{
"code": 200,
"message": "댓글 좋아요를 취소했습니다.",
"data": null
}게시글에 사용할 이미지를 업로드합니다.
- 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 {
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 | 최신 아이폰 기본 형식 | 고품질 사진, 최신 모바일 |
✅ 성공 응답:
{
"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 | 서버 내부 오류 |
POST /posts/upload/video
게시글에 첨부할 영상을 업로드합니다. 단일 영상만 업로드 가능합니다.
| 헤더 | 타입 | 필수 | 설명 |
|---|---|---|---|
| Authorization | string | 필수 | Bearer 토큰 |
| Content-Type | string | 필수 | multipart/form-data |
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| 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);
}
};✅ 성공 응답 (썸네일 생성 성공):
{
"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
- 이미지:
/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_atPostContent.post_id+PostContent.sequencePostLike.post_id+PostLike.user_idPostBookmark.post_id+PostBookmark.user_idPostTag.post_id+PostTag.tag_idTag.name(Full-text search용)
- 게시글 목록: 1분 캐시 (Redis)
- 게시글 상세: 5분 캐시 (Redis)
- 좋아요/북마크 상태: 10분 캐시 (Redis)
- 태그 목록: 1시간 캐시 (Redis)
- Full-text search 인덱스 (제목, 내용)
- 태그 기반 빠른 필터링
- 사용자별 맞춤 추천 알고리즘 (향후 구현)
- 커서 기반 페이지네이션 (created_at 기준)
- 인덱스를 활용한 빠른 조회
- 최대 50개까지 한 번에 조회 가능
- WebP 포맷 우선 사용
- 썸네일 자동 생성 (100x100, 300x300)
- CDN 연동 (향후 구현)
GET /posts/animal-types
서버에서 관리하는 동물 타입 목록을 조회합니다. 클라이언트는 이 API를 통해 드롭다운이나 선택 UI를 동적으로 구성할 수 있습니다.
GET /posts/animal-types
{
"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": "기타"
}
]
}
}| Field | Type | Required | Description |
|---|---|---|---|
| animal_types | array | 필수 | 동물 타입 목록 |
| animal_types[].value | string | 필수 | 동물 타입 값 (enum 값) |
| animal_types[].label | string | 필수 | 동물 타입 표시명 (UI 표시용) |
- 서버 관리: 동물 타입을 서버에서 중앙 집중 관리
- 클라이언트 하드코딩 제거: 클라이언트가 enum 값을 직접 정의하지 않아도 됨
- 확장성: 새로운 동물 타입 추가 시 클라이언트 변경 불필요
- 다국어 지원: label을 현지화하여 다국어 지원 가능
POST /posts/upload/video/edit
업로드된 영상을 trim과 crop 파라미터로 편집하고, 편집된 영상과 썸네일을 생성합니다.
상세한 API 명세는 피드 영상 편집 업로드 API(/feeds/upload/video/edit)를 참고해주세요. 동일한 방식으로 동작합니다.