Maple Talk SNS 관리자 서버의 REST API 명세서입니다. 모든 API는 일관된 응답 형식을 따르며, 웹 전용으로 설계되었습니다.
{
"code": 200,
"message": "성공 메시지",
"data": {
// 실제 응답 데이터
}
}{
"code": 400,
"message": "에러 메시지",
"errors": [
{
"field": "필드명",
"message": "상세 에러 메시지"
}
]
}- 토큰 저장: httpOnly 쿠키로 자동 저장
- 토큰 전송: 쿠키로 자동 전송
- 보안: XSS, CSRF 공격 방지
- 사용법: 별도 설정 없이 자동 작동
- 등록된 Admin만 로그인: 미리 등록된 카카오 계정만 허용
- 토큰 해시 저장: RefreshToken 보안 강화
- 역할 기반 권한: OWNER > ADMIN > CS > READONLY
- 계정 잠금: 로그인 실패 횟수 제한 (5회)
| 역할 | 등급 | 권한 설명 |
|---|---|---|
| OWNER | 4 | 최고 권한 (모든 기능 접근 가능) |
| ADMIN | 3 | 관리자 권한 (대부분 기능 접근 가능) |
| CS | 2 | 고객지원 권한 (조회/수정 권한) |
| READONLY | 1 | 읽기 전용 권한 (조회만 가능) |
Endpoint: GET /admin/users
Description: 관리자가 시스템에 등록된 모든 사용자 목록을 조회합니다. 삭제되지 않은 사용자만 표시되며 페이지네이션이 지원됩니다.
Query Parameters:
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| page | number | ❌ | 1 | 페이지 번호 (1부터 시작) |
| limit | number | ❌ | 20 | 한 페이지당 항목 수 (최대 100) |
| search | string | ❌ | - | 검색어 (닉네임, 이메일에서 검색) |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. 쿼리 파라미터 검증 및 기본값 설정
3. User 테이블에서 삭제되지 않은 사용자 조회
4. 이미지 URL에 baseURL 추가
5. 페이지네이션 정보 계산
6. 스네이크 케이스 변환 후 응답
Response (200 OK):
{
"code": 200,
"message": "사용자 목록을 조회했습니다.",
"data": {
"users": [
{
"id": 1,
"email": "user@example.com",
"nickname": "사용자닉네임",
"profile_img": "https://sa-server-u2xi.onrender.com/uploads/profile/image.jpg",
"bio": "자기소개 텍스트",
"profile_visibility": "public",
"is_deleted": 0,
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z",
"login_type": "social",
"providers": ["kakao"]
},
{
"id": 2,
"email": "local@example.com",
"nickname": "로컬사용자",
"profile_img": null,
"bio": null,
"profile_visibility": "public",
"is_deleted": 0,
"created_at": "2024-01-02T00:00:00.000Z",
"updated_at": "2024-01-02T00:00:00.000Z",
"login_type": "local",
"providers": []
}
],
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_count": 50,
"per_page": 10,
"has_next": true,
"has_prev": false
}
}
}페이지네이션 상세:
- current_page: 현재 페이지 번호
- total_pages: 전체 페이지 수
- total_count: 전체 사용자 수
- per_page: 한 페이지당 항목 수
- has_next: 다음 페이지 존재 여부
- has_prev: 이전 페이지 존재 여부
필터링:
is_deleted = 0→ 삭제되지 않은 사용자만 표시- 정렬:
created_at DESC(최신 사용자 우선)
이미지 URL 처리:
- DB에 저장된 상대 경로를 완전한 URL로 변환
profile_img: "/uploads/profile/image.jpg"→"https://sa-server-u2xi.onrender.com/uploads/profile/image.jpg"
Error Responses:
401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "사용자 목록 조회 중 오류가 발생했습니다.",
"errors": []
}Endpoint: GET /admin/inquiries
Description: 관리자가 시스템에 등록된 모든 문의 내역을 조회합니다. 페이지네이션과 필터링이 지원됩니다.
Query Parameters:
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| page | number | ❌ | 1 | 페이지 번호 (1부터 시작) |
| limit | number | ❌ | 20 | 한 페이지당 항목 수 (최대 100) |
| status | string | ❌ | - | 상태 필터 (pending, in_progress, resolved, closed) |
| priority | string | ❌ | - | 우선순위 필터 (low, normal, high, urgent) |
| search | string | ❌ | - | 검색어 (제목, 내용에서 검색) |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. 쿼리 파라미터 검증 및 필터링 적용
3. Inquiry와 User, InquiryAttachment LEFT JOIN
4. 첨부파일 URL에 baseURL 추가
5. 페이지네이션 정보 계산
6. 스네이크 케이스 변환 후 응답
Response (200 OK):
{
"code": 200,
"message": "문의 내역을 조회했습니다.",
"data": {
"inquiries": [
{
"id": 1,
"user": {
"id": 10,
"nickname": "사용자닉네임",
"email": "user@example.com"
},
"category": "계정문제",
"title": "로그인할 수 없습니다",
"content": "로그인이 안 돼요...",
"status": "pending",
"priority": "normal",
"attachments": [
{
"id": 1,
"filename": "error_screenshot.png",
"file_url": "https://sa-server-u2xi.onrender.com/uploads/inquiry/error_screenshot.png"
}
],
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
],
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_count": 50,
"per_page": 10,
"has_next": true,
"has_prev": false
}
}
}필터링 예시:
# 대기 중인 문의만 조회
GET /admin/inquiries?status=pending
# 긴급 우선순위 문의 조회
GET /admin/inquiries?priority=urgent
# 처리 중인 문의, 2페이지
GET /admin/inquiries?status=in_progress&page=2&limit=10JOIN 정보:
- User: 문의한 사용자 정보 (INNER JOIN)
- InquiryAttachment: 첨부파일 정보 (LEFT JOIN)
이미지 URL 처리:
- 첨부파일의 상대 경로를 완전한 URL로 변환
정렬:
created_at DESC(최신 문의 우선)
Error Responses:
401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "문의 내역 조회 중 오류가 발생했습니다.",
"errors": []
}Endpoint: GET /admin/posts
Description: 관리자가 시스템에 등록된 모든 게시물 목록을 조회합니다. 페이지네이션과 필터링이 지원됩니다.
Query Parameters:
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| page | number | ❌ | 1 | 페이지 번호 (1부터 시작) |
| limit | number | ❌ | 20 | 한 페이지당 항목 수 (최대 100) |
| category | string | ❌ | - | 상위 카테고리 ID 필터 |
| sub_category | string | ❌ | - | 서브 카테고리 ID 필터 |
| search | string | ❌ | - | 제목 검색 |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. 쿼리 파라미터 검증 및 필터링 적용
3. Post와 User, SubCategory, Category JOIN
4. 페이지네이션 정보 계산
5. 스네이크 케이스 변환 후 응답
Response (200 OK):
{
"code": 200,
"message": "게시물 목록을 조회했습니다.",
"data": {
"posts": [
{
"id": 1,
"title": "게시물 제목입니다",
"user": {
"id": 10,
"nickname": "작성자닉네임",
"email": "user@example.com"
},
"category": {
"id": 1,
"name": "카테고리명",
"sub_category": {
"id": 5,
"name": "서브카테고리명"
}
},
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
],
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_count": 50,
"per_page": 10,
"has_next": true,
"has_prev": false
}
}
}필터링 예시:
# 특정 카테고리의 게시물만 조회
GET /admin/posts?category=1
# 특정 서브카테고리의 게시물 조회
GET /admin/posts?sub_category=5
# 제목 검색
GET /admin/posts?search=게시물
# 복합 필터링
GET /admin/posts?category=1&search=질문&page=2&limit=10JOIN 정보:
- User: 게시물 작성자 정보 (INNER JOIN)
- SubCategory: 서브 카테고리 정보 (INNER JOIN)
- Category: 상위 카테고리 정보 (INNER JOIN, SubCategory를 통해)
정렬:
created_at DESC(최신 게시물 우선)
Error Responses:
401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "게시물 목록 조회 중 오류가 발생했습니다.",
"errors": []
}Endpoint: GET /admin/posts/:postId
Description: 관리자가 특정 게시물의 상세 정보를 조회합니다. 게시물 내용, 작성자 정보, 카테고리 정보를 포함합니다.
Path Parameters:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| postId | number | ✅ | 게시물 ID |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. postId 파라미터 검증
3. Post와 User, SubCategory, Category, PostContent JOIN
4. 이미지/비디오 URL에 baseURL 추가
5. 스네이크 케이스 변환 후 응답
Response (200 OK):
{
"code": 200,
"message": "게시물을 조회했습니다.",
"data": {
"id": 1,
"title": "게시물 제목입니다",
"user": {
"id": 10,
"nickname": "작성자닉네임",
"email": "user@example.com"
},
"category": {
"id": 1,
"name": "카테고리명",
"sub_category": {
"id": 5,
"name": "서브카테고리명"
}
},
"content_blocks": [
{
"type": "text",
"value": "텍스트 내용입니다.",
"sequence": 1
},
{
"type": "image",
"value": "https://sa-server-u2xi.onrender.com/uploads/post/image.jpg",
"sequence": 2
},
{
"type": "video",
"value": "https://sa-server-u2xi.onrender.com/uploads/post/video.mp4",
"thumbnail_url": "https://sa-server-u2xi.onrender.com/uploads/post/thumbnail.jpg",
"sequence": 3
}
],
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
}JOIN 정보:
- User: 게시물 작성자 정보 (INNER JOIN)
- SubCategory: 서브 카테고리 정보 (INNER JOIN)
- Category: 상위 카테고리 정보 (INNER JOIN, SubCategory를 통해)
- PostContent: 게시물 내용 블록들 (별도 조회)
이미지 URL 처리:
- DB에 저장된 상대 경로를 완전한 URL로 변환
content_blocks[].value: 텍스트 또는 이미지/비디오 경로content_blocks[].thumbnail_url: 비디오 썸네일 경로
Error Responses:
400 Bad Request - 잘못된 postId:
{
"code": 400,
"message": "올바른 게시물 ID를 입력해주세요.",
"errors": []
}401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}404 Not Found - 게시물 없음:
{
"code": 404,
"message": "게시물을 찾을 수 없습니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "게시물 조회 중 오류가 발생했습니다.",
"errors": []
}Endpoint: DELETE /admin/posts/:postId
Description: 관리자가 특정 게시물을 삭제합니다. 게시물과 관련된 모든 데이터(콘텐츠, 좋아요, 북마크, 댓글 등)를 함께 삭제합니다.
Path Parameters:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| postId | number | ✅ | 삭제할 게시물 ID |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. postId 파라미터 검증
3. 게시물 존재 확인
4. 트랜잭션 시작
5. PostContent 삭제
6. PostLike 삭제
7. PostBookmark 삭제
8. Comment 및 CommentLike 삭제
9. Post 삭제
10. 트랜잭션 커밋
Response (200 OK):
{
"code": 200,
"message": "게시물이 삭제되었습니다.",
"data": {
"success": true
}
}삭제되는 데이터:
- 게시물 (Post)
- 게시물 내용 (PostContent)
- 좋아요 (PostLike)
- 북마크 (PostBookmark)
- 댓글 (Comment)
- 댓글 좋아요 (CommentLike)
Error Responses:
400 Bad Request - 잘못된 postId:
{
"code": 400,
"message": "올바른 게시물 ID를 입력해주세요.",
"errors": []
}401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}404 Not Found - 게시물 없음:
{
"code": 404,
"message": "게시물을 찾을 수 없습니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "게시물 삭제 중 오류가 발생했습니다.",
"errors": []
}Endpoint: GET /admin/feeds
Description: 관리자가 시스템에 등록된 모든 피드 목록을 조회합니다. 기존 사용자 피드 조회와 달리 팔로워 제한 없이 모든 피드를 표시합니다. 페이지네이션과 필터링이 지원됩니다.
Query Parameters:
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| page | number | ❌ | 1 | 페이지 번호 (1부터 시작) |
| limit | number | ❌ | 20 | 한 페이지당 항목 수 (최대 100) |
| user_id | string | ❌ | - | 특정 사용자 피드 필터 |
| search | string | ❌ | - | 내용 검색 (텍스트 블록에서 검색) |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. 쿼리 파라미터 검증 및 필터링 적용
3. FeedPost와 User JOIN (팔로워 제한 없음)
4. 각 피드의 FeedPostContent 조회 및 URL 완성
5. 검색 필터 적용 (content_blocks 텍스트 검색)
6. 페이지네이션 정보 계산
7. 스네이크 케이스 변환 후 응답
Response (200 OK):
{
"code": 200,
"message": "피드 목록을 조회했습니다.",
"data": {
"feeds": [
{
"id": 1,
"user": {
"id": 10,
"nickname": "작성자닉네임",
"email": "user@example.com"
},
"content_blocks": [
{
"type": "text",
"value": "피드 내용입니다.",
"sequence": 1
},
{
"type": "image",
"value": "https://sa-server-u2xi.onrender.com/uploads/feed/image.jpg",
"sequence": 2
},
{
"type": "video",
"value": "https://sa-server-u2xi.onrender.com/uploads/feed/video.mp4",
"thumbnail_url": "https://sa-server-u2xi.onrender.com/uploads/feed/thumbnail.jpg",
"sequence": 3
}
],
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
],
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_count": 50,
"per_page": 10,
"has_next": true,
"has_prev": false
}
}
}필터링 예시:
# 특정 사용자의 피드만 조회
GET /admin/feeds?user_id=10
# 내용 검색
GET /admin/feeds?search=안녕하세요
# 복합 필터링
GET /admin/feeds?user_id=10&search=사진&page=2&limit=10JOIN 정보:
- User: 피드 작성자 정보 (INNER JOIN)
검색 대상:
- content_blocks의
type: "text"인 블록들의value내용
이미지 URL 처리:
- DB에 저장된 상대 경로를 완전한 URL로 변환
content_blocks[].value: 텍스트 또는 이미지/비디오 경로content_blocks[].thumbnail_url: 비디오 썸네일 경로
차이점 (vs 일반 피드 조회):
- 팔로워 제한 없음 (모든 피드 표시)
- 관리자 전용 필터링 옵션
- 검색 기능 포함
정렬:
created_at DESC(최신 피드 우선)
Error Responses:
401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "피드 목록 조회 중 오류가 발생했습니다.",
"errors": []
}Endpoint: DELETE /admin/feeds/:feedId
Description: 관리자가 특정 피드를 삭제합니다. 피드와 관련된 모든 데이터(콘텐츠, 좋아요, 북마크, 댓글 등)를 함께 삭제합니다.
Path Parameters:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| feedId | number | ✅ | 삭제할 피드 ID |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. feedId 파라미터 검증
3. 피드 존재 확인
4. 트랜잭션 시작
5. FeedPostContent 삭제
6. FeedLike 삭제
7. FeedBookmark 삭제
8. FeedComment 및 FeedCommentLike 삭제
9. FeedPost 삭제
10. 트랜잭션 커밋
Response (200 OK):
{
"code": 200,
"message": "피드가 삭제되었습니다.",
"data": {
"success": true
}
}삭제되는 데이터:
- 피드 (FeedPost)
- 피드 내용 (FeedPostContent)
- 좋아요 (FeedLike)
- 북마크 (FeedBookmark)
- 댓글 (FeedComment)
- 댓글 좋아요 (FeedCommentLike)
Error Responses:
400 Bad Request - 잘못된 feedId:
{
"code": 400,
"message": "올바른 피드 ID를 입력해주세요.",
"errors": []
}401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}404 Not Found - 피드 없음:
{
"code": 404,
"message": "피드를 찾을 수 없습니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "피드 삭제 중 오류가 발생했습니다.",
"errors": []
}Endpoint: GET /admin/reports
Description: 관리자가 시스템에 등록된 모든 신고 목록을 조회합니다. 페이지네이션과 필터링이 지원됩니다.
Query Parameters:
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| page | number | ❌ | 1 | 페이지 번호 (1부터 시작) |
| limit | number | ❌ | 20 | 한 페이지당 항목 수 (최대 100) |
| status | string | ❌ | - | 상태 필터 (pending, resolved, dismissed) |
| target_type | string | ❌ | - | 대상 타입 필터 (post, feed_post, comment, feed_comment, short, short_comment, user, chat_room, message, story) |
| category | string | ❌ | - | 카테고리 필터 (spam, abuse, nudity, fraud, copyright, harassment, hate_speech, other) |
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. 쿼리 파라미터 검증 및 기본값 설정
3. Report와 User, Admin LEFT JOIN
4. 페이지네이션 정보 계산
5. 스네이크 케이스 변환 후 응답
Response (200 OK):
{
"code": 200,
"message": "신고 목록을 조회했습니다.",
"data": {
"reports": [
{
"id": 1,
"reporter_id": 10,
"reporter_nickname": "신고자닉네임",
"target_type": "post",
"target_id": 100,
"category": "spam",
"reason": "스팸 게시물입니다",
"status": "pending",
"admin_id": null,
"admin_nickname": null,
"admin_note": null,
"resolved_at": null,
"target_snapshot": {
"target_type": "post",
"target_id": 100,
"created_at": "2024-01-01T00:00:00.000Z"
},
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
],
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_count": 50,
"per_page": 10,
"has_next": true,
"has_prev": false
}
}
}필터링 예시:
# 대기 중인 신고만 조회
GET /admin/reports?status=pending
# 특정 대상 타입의 신고 조회
GET /admin/reports?target_type=post
# 스팸 카테고리 신고 조회
GET /admin/reports?category=spam
# 복합 필터링
GET /admin/reports?status=pending&target_type=post&category=spam&page=2&limit=10JOIN 정보:
- User: 신고자 정보 (INNER JOIN)
- Admin: 처리한 관리자 정보 (LEFT JOIN)
정렬:
created_at DESC(최신 신고 우선)
Error Responses:
401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "신고 목록 조회 중 오류가 발생했습니다.",
"errors": []
}Endpoint: GET /admin/reports/stats
Description: 관리자가 신고에 대한 통계 정보를 조회합니다. 상태별, 카테고리별, 대상 타입별 통계를 제공합니다.
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. Report 테이블에서 GROUP BY 집계 쿼리 실행
3. 상태별, 카테고리별, 대상 타입별 통계 계산
4. 스네이크 케이스 변환 후 응답
Response (200 OK):
{
"code": 200,
"message": "신고 통계를 조회했습니다.",
"data": {
"total_reports": 150,
"pending_reports": 80,
"resolved_reports": 50,
"dismissed_reports": 20,
"reports_by_category": {
"spam": 60,
"abuse": 30,
"nudity": 20,
"fraud": 15,
"copyright": 10,
"harassment": 10,
"hate_speech": 5,
"other": 10
},
"reports_by_target_type": {
"post": 50,
"feed_post": 30,
"comment": 25,
"feed_comment": 20,
"short": 15,
"short_comment": 10,
"user": 5,
"chat_room": 3,
"message": 1,
"story": 1
}
}
}통계 항목 설명:
- total_reports: 총 신고 수
- pending_reports: 대기 중인 신고 수
- resolved_reports: 처리된 신고 수
- dismissed_reports: 기각된 신고 수
- reports_by_category: 카테고리별 신고 수
- reports_by_target_type: 대상 타입별 신고 수
Error Responses:
401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "신고 통계 조회 중 오류가 발생했습니다.",
"errors": []
}Endpoint: POST /admin/reports/:reportId/process
Description: 관리자가 특정 신고를 처리합니다. 신고 상태를 변경하고 관리자 메모를 추가할 수 있습니다.
Path Parameters:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| reportId | number | ✅ | 처리할 신고 ID |
Request Headers:
Cookie: admin_access={accessToken}
Request Body:
{
"status": "resolved",
"admin_note": "사용자 차단 및 게시물 삭제 조치 완료"
}Body Parameters:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| status | string | ✅ | 처리 상태 (resolved, dismissed) |
| admin_note | string | ❌ | 관리자 메모 |
동작 방식:
1. Admin 토큰 검증 (adminAuthMiddleware)
2. reportId 파라미터 검증
3. 신고 존재 확인 및 상태 검증 (pending만 처리 가능)
4. 트랜잭션 시작
5. 신고 상태 업데이트 (status, admin_id, admin_note, resolved_at)
6. 트랜잭션 커밋
Response (200 OK):
{
"code": 200,
"message": "신고가 처리되었습니다.",
"data": null
}Error Responses:
400 Bad Request - 잘못된 reportId:
{
"code": 400,
"message": "올바른 신고 ID를 입력해주세요.",
"errors": []
}400 Bad Request - 잘못된 상태 값:
{
"code": 400,
"message": "올바른 처리 상태를 입력해주세요.",
"errors": []
}401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}404 Not Found - 신고 없음:
{
"code": 404,
"message": "신고를 찾을 수 없습니다.",
"errors": []
}404 Not Found - 이미 처리됨:
{
"code": 404,
"message": "이미 처리된 신고입니다.",
"errors": []
}500 Internal Server Error:
{
"code": 500,
"message": "신고 처리 중 오류가 발생했습니다.",
"errors": []
}[Admin Web]
↓ (관리자 로그인 버튼)
GET /admin/auth/kakao/start
↓ (302 redirect + state 쿠키)
카카오 로그인 페이지
↓ (사용자 로그인)
카카오 redirect
GET /admin/auth/kakao/callback?code=AUTH_CODE&state=STATE
↓
[Server] CSRF 검증 → 토큰 교환 → Admin 검증 → 쿠키 설정
↓ (302 redirect)
프론트엔드 대시보드 (${ADMIN_WEB_URL}/dashboard)
admin_access: HttpOnly; Secure; SameSite=Lax (15분)admin_refresh: HttpOnly; Secure; SameSite=Strict; Path=/admin/auth/refresh (15일)admin_oauth_state: HttpOnly; SameSite=Lax (10분, 일회용)
Endpoint: GET /admin/auth/kakao/start
Description: 카카오 OAuth 로그인을 시작합니다. CSRF 방지용 state를 생성하고 카카오 로그인 페이지로 리다이렉트합니다.
Request Headers: 없음
Request Body: 없음
동작 방식:
1. CSRF 방지용 state 생성 (32바이트 랜덤)
2. state를 admin_oauth_state 쿠키에 저장
3. 카카오 OAuth 인증 URL 생성 (state 포함)
4. 카카오 로그인 페이지로 302 리다이렉트
카카오 OAuth URL 예시:
GET https://kauth.kakao.com/oauth/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state={STATE}
Response: 302 리다이렉트 (카카오 로그인 페이지)
Error Responses:
500 Internal Server Error - 환경변수 누락:
{
"code": 500,
"message": "카카오 로그인 시작 중 오류가 발생했습니다.",
"errors": []
}Endpoint: GET /admin/auth/kakao/callback
Description: 카카오 OAuth 콜백을 처리합니다. authorization code와 state를 검증하고 Admin 인증을 완료합니다.
Query Parameters:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| code | string | ✅ | 카카오 authorization code |
| state | string | ✅ | CSRF 방지용 state 값 |
| error | string | ❌ | 카카오 에러 코드 (실패 시) |
동작 방식:
1. authorization code와 state 검증
2. 저장된 state 쿠키와 비교 (CSRF 방지)
3. state 쿠키 삭제 (일회용)
4. code → 카카오 access_token 교환
5. 카카오 사용자 정보 조회
6. kakao_user_id로 Admin 테이블 검색
7. Admin이 없으면 "관리자 권한이 없습니다" 에러
8. 계정 잠금 상태 확인
9. 기존 RefreshToken 삭제
10. JWT 토큰 생성 및 해시 저장
11. 로그인 시각 및 실패 카운트 초기화
12. admin_access/admin_refresh 쿠키 설정
13. 프론트엔드 대시보드로 리다이렉트
웹 쿠키 설정:
admin_access: HttpOnly; Secure; SameSite=Lax (15분)admin_refresh: HttpOnly; Secure; SameSite=Strict; Path=/admin/auth/refresh (15일)
Response: 302 리다이렉트
성공 시 리다이렉트: ${ADMIN_WEB_URL}/dashboard
실패 시 리다이렉트:
${ADMIN_WEB_URL}/login?error=not_admin- 관리자 권한 없음${ADMIN_WEB_URL}/login?error=account_locked- 계정 잠금${ADMIN_WEB_URL}/login?error=invalid_state- CSRF 공격 감지${ADMIN_WEB_URL}/login?error=login_failed- 기타 로그인 실패
Error Handling:
- 카카오 에러 →
kakao_oauth_failed - Authorization code 누락 →
no_authorization_code - State 불일치 →
invalid_state
Endpoint: POST /admin/auth/refresh
Description: RefreshToken을 사용하여 새로운 AccessToken을 발급합니다. 해시된 토큰을 검증합니다.
Request Headers:
Cookie: admin_refresh={refreshToken}
Request Body: 없음
동작 방식:
1. 쿠키에서 admin_refresh 추출
2. RefreshToken 검증 (admin_refresh 타입 확인)
3. 토큰 해시 생성 및 DB 비교
4. 토큰 만료/취소 상태 확인
5. Admin 존재 및 활성 상태 확인
6. 새 AccessToken 생성
Response (200 OK):
{
"code": 200,
"message": "토큰이 재발급되었습니다.",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}Error Responses:
400 Bad Request - RefreshToken 누락:
{
"code": 400,
"message": "관리자 RefreshToken이 필요합니다.",
"errors": []
}401 Unauthorized - 토큰 만료:
{
"code": 401,
"message": "관리자 RefreshToken이 만료되었습니다.",
"errors": [
{
"field": "refreshToken",
"message": "TOKEN_EXPIRED"
}
]
}401 Unauthorized - 토큰 취소됨:
{
"code": 401,
"message": "유효하지 않은 관리자 RefreshToken입니다.",
"errors": []
}Endpoint: POST /admin/auth/logout
Description: 관리자 계정에서 로그아웃하고 모든 토큰을 무효화합니다.
Request Headers:
Cookie: admin_access={accessToken}
Cookie: admin_refresh={refreshToken}
Request Body: 없음
동작 방식:
1. AccessToken 검증 시도
2. RefreshToken 검증 시도 (fallback)
3. adminId 추출
4. DB에서 해당 adminId의 모든 RefreshToken 삭제
5. 쿠키 삭제 지시
Response (200 OK):
{
"code": 200,
"message": "관리자 로그아웃이 완료되었습니다.",
"data": null
}웹 쿠키 삭제:
admin_access쿠키 삭제admin_refresh쿠키 삭제
특별 케이스 - 토큰 없음:
{
"code": 200,
"message": "로그아웃이 완료되었습니다. (클라이언트 정리만 진행됨)",
"data": null
}Endpoint: GET /admin/auth/me
Description: 현재 로그인된 관리자의 정보를 조회합니다.
Request Headers:
Cookie: admin_access={accessToken}
Request Body: 없음
Response (200 OK):
{
"code": 200,
"message": "관리자 정보 조회가 완료되었습니다.",
"data": {
"id": 1,
"email": "admin@kakao.com",
"name": "관리자",
"role": "OWNER",
"last_login_at": "2025-12-29T12:30:00.000Z",
"created_at": "2025-01-01T00:00:00.000Z"
}
}Error Responses:
401 Unauthorized - 인증 실패:
{
"code": 401,
"message": "관리자 인증 토큰이 필요합니다.",
"errors": []
}404 Not Found - 관리자 없음:
{
"code": 404,
"message": "관리자를 찾을 수 없습니다.",
"errors": []
}- 유효기간: 15분
- 용도: Admin API 요청 인증
- 포함 정보: adminId, type: 'admin_access'
- 유효기간: 15일
- 용도: AccessToken 재발급
- 저장 방식: SHA-256 해시 저장 (보안 강화)
- 포함 정보: adminId, type: 'admin_refresh'
- 카카오 로그인 시작
# 브라우저에서 직접 접근
GET http://localhost:3001/admin/auth/kakao/start
# → 카카오 로그인 페이지로 리다이렉트 (state 쿠키 자동 설정)- 카카오 로그인 완료
사용자가 카카오 계정으로 로그인
→ 카카오가 콜백 URL로 리다이렉트 (?code=xxx&state=yyy)
→ 서버에서 CSRF 검증 후 Admin 인증
→ admin_access/admin_refresh 쿠키 설정
→ 프론트엔드 대시보드로 리다이렉트
- 관리자 정보 조회
curl -X GET http://localhost:3001/admin/auth/me \
--cookie "admin_access=token_here"- 토큰 재발급
curl -X POST http://localhost:3001/admin/auth/refresh \
--cookie "admin_refresh=token_here"- 로그아웃
curl -X POST http://localhost:3001/admin/auth/logout \
--cookie "admin_access=token_here"- 용도: Admin API 인증 (AccessToken 검증)
- 토큰 위치: 쿠키의
admin_access - 타입 확인:
admin_access타입 검증
- 용도: 토큰 재발급 전용 (RefreshToken 해시 검증)
- 토큰 위치: 쿠키의
admin_refresh - 해시 검증: DB에서 SHA-256 해시 비교
- 타입 확인:
admin_refresh타입 검증
- 용도: 향상된 로그아웃 (AccessToken + RefreshToken 활용)
- 특징: 토큰 만료되어도 adminId 추출 가능
- fallback: 둘 다 실패시 graceful degradation
// 역할 기반 권한 제어
app.get('/admin/users',
adminAuthMiddleware,
adminRoleMiddleware(['ADMIN', 'OWNER']),
getUsersHandler
);| API | 미들웨어 | 설명 |
|---|---|---|
/admin/auth/kakao/start |
없음 | 인증 불필요 (OAuth 시작) |
/admin/auth/kakao/callback |
없음 | 인증 불필요 (OAuth 콜백) |
/admin/auth/refresh |
adminRefreshTokenMiddleware |
RefreshToken 해시 검증 |
/admin/auth/logout |
adminLogoutMiddleware |
향상된 로그아웃 |
/admin/auth/me |
adminAuthMiddleware |
AccessToken 검증 |
/admin/users |
adminAuthMiddleware |
AccessToken 검증 |
/admin/inquiries |
adminAuthMiddleware |
AccessToken 검증 |
/admin/posts |
adminAuthMiddleware |
AccessToken 검증 |
/admin/posts/:postId |
adminAuthMiddleware |
AccessToken 검증 |
/admin/feeds |
adminAuthMiddleware |
AccessToken 검증 |
/admin/feeds/:feedId |
adminAuthMiddleware |
AccessToken 검증 |
| HTTP 상태 코드 | 설명 | 사용 예시 |
|---|---|---|
| 200 | OK | 성공적인 요청 처리 |
| 400 | Bad Request | 잘못된 요청 (입력값 오류) |
| 401 | Unauthorized | 인증 실패 (토큰 무효) |
| 403 | Forbidden | 권한 없음 (미등록 Admin) |
| 404 | Not Found | 리소스 없음 (Admin 계정 없음) |
| 423 | Locked | 계정 잠금 (실패 횟수 초과) |
| 500 | Internal Server Error | 서버 내부 오류 |
토큰 만료:
{
"code": 401,
"message": "관리자 인증에 실패했습니다.",
"errors": [
{
"field": "token",
"message": "TOKEN_EXPIRED"
}
]
}잘못된 토큰 타입:
{
"code": 401,
"message": "유효하지 않은 관리자 토큰입니다.",
"errors": []
}토큰 만료:
{
"code": 401,
"message": "관리자 RefreshToken이 만료되었습니다.",
"errors": [
{
"field": "refreshToken",
"message": "TOKEN_EXPIRED"
}
]
}해시 불일치 (토큰 변조):
{
"code": 401,
"message": "유효하지 않은 관리자 RefreshToken입니다.",
"errors": []
}- 해시 저장: RefreshToken을 SHA-256으로 해시하여 저장
- 등록 필터링: 미등록 카카오 계정 접근 거부
- 실패 제한: 로그인 실패 횟수 제한 및 계정 잠금
- 토큰 로테이션: 로그인 시 기존 토큰 자동 무효화
- Admin 테이블: 관리자 기본 정보
- AdminRefreshTokens 테이블: 해시된 토큰 및 로테이션 추적
- 외래 키 제약: CASCADE 삭제 지원
- 단일 세션: 로그인 시 기존 RefreshToken 자동 삭제
- 토큰 만료: AccessToken 15분, RefreshToken 15일
- 강제 로그아웃: 보안 위협 시 수동 토큰 무효화 가능
- 데이터베이스: MySQL (maple_talk_sns)
- ORM: Sequelize
- 인증 방식: JWT (JSON Web Token) + 해시
- 카카오 API: 사용자 정보 조회 및 검증
- 플랫폼: 웹 브라우저 전용
- CORS: 웹 도메인 허용 (localhost:3000 등)
INSERT INTO Admins (
kakao_user_id,
email,
name,
role,
is_active
) VALUES (
123456789, -- 카카오 사용자 ID
'admin@kakao.com',
'관리자',
'OWNER',
1
);// OWNER 권한 필요
POST /admin/register
{
"kakao_user_id": 123456789,
"email": "admin@kakao.com",
"name": "관리자",
"role": "ADMIN"
}- 카카오 개발자센터 접속
- 애플리케이션 추가 → 웹 플랫폼 선택
- 앱 키 → REST API 키 복사 (CLIENT_ID)
- 앱 키 → Client Secret 복사 (CLIENT_SECRET)
- Redirect URI 설정:
http://localhost:3001/admin/auth/kakao/callback
# .env 파일에 추가
KAKAO_CLIENT_ID=카카오_REST_API_키 # 필수: 카카오 콘솔 REST API 키
KAKAO_CLIENT_SECRET=카카오_Client_Secret # 선택: Client Secret (설정된 경우만)
KAKAO_REDIRECT_URI=http://localhost:3001/admin/auth/kakao/callback # 필수: 콜백 URL
ADMIN_WEB_URL=http://localhost:5173 # 필수: 프론트엔드 URL (React/Vite)KAKAO_CLIENT_ID: 카카오 개발자센터 → 앱 설정 → 앱 키 → REST API 키 값KAKAO_CLIENT_SECRET: 카카오 개발자센터 → 앱 설정 → 앱 키 → Client Secret 값 (선택사항)KAKAO_REDIRECT_URI: 카카오 개발자센터에 등록된 콜백 URL과 완전히 동일해야 함ADMIN_WEB_URL: React/Vite 프론트엔드 URL (로그인 성공/실패 시 리다이렉트)
INSERT INTO Admins (kakao_user_id, email, name, role, is_active)
VALUES (카카오_사용자_ID, 'admin@kakao.com', '관리자', 'OWNER', 1);# 서버 재시작
npm run dev
# 브라우저에서 테스트
http://localhost:3001/admin/auth/kakao/start// React에서 로그인 버튼
const handleLogin = () => {
window.location.href = '/admin/auth/kakao/start';
};
// 로그아웃
const handleLogout = async () => {
await fetch('/admin/auth/logout', { method: 'POST' });
// 쿠키 자동 삭제됨
};Admins.kakao_user_id(Unique)Admins.is_active+Admins.kakao_user_idAdminRefreshTokens.token_hash(Unique)AdminRefreshTokens.admin_id+AdminRefreshTokens.revoked_at
- Admin 기본 정보: 10분 캐싱
- 토큰 검증 결과: 5분 캐싱
- 역할 권한 정보: 영구 캐싱
- 토큰 해시 검증 (DB 부하 감소)
- 로그인 실패 횟수 메모리 캐싱
- IP 기반 접근 제한 (향후 구현)
이 문서는 Maple Talk SNS Admin API의 완전한 명세서를 제공합니다. 보안과 안정성을 최우선으로 설계되었습니다.