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์ ์์ ํ ๋ช ์ธ์๋ฅผ ์ ๊ณตํฉ๋๋ค. ๋ณด์๊ณผ ์์ ์ฑ์ ์ต์ฐ์ ์ผ๋ก ์ค๊ณ๋์์ต๋๋ค.