
목차
1. 개요 및 특징2. 설치 방법
3. 인증 방식 (JWT Bearer Token)
4. 응답 표준 형식
5. 전체 엔드포인트 목록 (22개)
6. 인증 API — /api/v1/auth/*
7. 게시판/게시글 API — /api/v1/boards/*
8. 댓글 API — /api/v1/posts/*
9. 회원 API — /api/v1/members/*
10. 파일 업로드 API — /api/v1/upload/*
11. 관리자 화면 사용법
12. 테스트 방법 (테스터 / cURL / Postman / React Native)
13. 다른 플러그인에서 API 확장하기
14. 오류 코드 목록
15. 보안 권장 사항
1. 개요 및 특징
DX API 플러그인은 DXCMS를 완전한 REST API 서버로 전환합니다. React Native, Flutter, Next.js, Vue 등 모든 프론트엔드와 외부 시스템에서 JSON으로 DXCMS 데이터를 활용할 수 있습니다.주요 특징
• Laravel 스타일 RESTful API — 동일한 URL 구조, 동일한 응답 형식
• JWT-like Bearer Token 인증 — Access Token(1시간) + Refresh Token(30일) + 토큰 로테이션
• 기존 DXCMS 코드 완전 비침습 — 웹 기능 그대로 동작
• 플러그인 비활성화 시 API 완전 종료 — 보안 제어 가능
• 훅 기반 확장 — 다른 플러그인이 엔드포인트 추가 가능 (dx_api_endpoints)
• CORS 자동 처리 — React Native, 웹앱 즉시 연동
• Rate Limiting — 로그인 분당 10회, 회원가입 분당 5회 제한
• 파일 업로드 — 이미지/파일 분리, 게시판 설정 자동 적용, 2단계 연결 방식
• PHP 5.6+ 호환 — 외부 라이브러리 없음
| 카테고리 | 엔드포인트 수 | 주요 기능 |
| 인증 (Auth) | 5개 | 로그인, 회원가입, 로그아웃, 토큰 갱신, 내 정보 |
| 게시판/게시글 | 7개 | 목록, 상세, 작성(file_ids 포함), 수정, 삭제 |
| 댓글 | 3개 | 목록, 작성, 삭제 |
| 회원 | 3개 | 프로필, 정보 수정, 비밀번호 변경 |
| 파일 업로드 | 4개 | 이미지 업로드, 파일 업로드, 게시글 연결, 삭제 |
2. 설치 방법
파일 구조
plugins/
dx-api/
manifest.php
plugin.php ← 훅 등록, DB 테이블 설치
admin/
index.php ← 관리자 화면 (대시보드 + 테스터)
core/
Request.php ← 입력 처리 (Laravel Request 유사)
Response.php ← JSON 응답 표준화
Router.php ← 라우팅 엔진 + 미들웨어
Token.php ← JWT 토큰 발급/검증
endpoints/
auth.php ← 인증 엔드포인트
boards.php ← 게시판/게시글 엔드포인트
comments.php ← 댓글 엔드포인트
members.php ← 회원 엔드포인트
upload.php ← 파일 업로드 엔드포인트
admin/
dx-api/
index.php ← 관리자 라우팅 진입점
core/api/
v1.php ← DXCMS dispatchApi() 진입점
설치 절차
1. plugins/dx-api/ 폴더를 DXCMS 루트의 plugins/ 폴더에 복사2. admin/dx-api/index.php 를 DXCMS 루트의 admin/dx-api/ 폴더에 복사
3. core/api/v1.php 를 DXCMS 루트의 core/api/ 폴더에 복사
4. 관리자 → 플러그인 → DX API → 활성화
5. 활성화 시 api_tokens 테이블 자동 생성
6. 관리자 → 사이트 → DX API 메뉴 확인
3. 인증 방식 (JWT Bearer Token)
DX API는 세션/쿠키 대신 Bearer Token 방식을 사용합니다. 인증이 필요한 모든 요청에 Authorization 헤더를 포함해야 합니다.토큰 종류
| 토큰 | 유효시간 | 저장 위치 | 용도 |
| Access Token | 1시간 (3600초) | 앱 메모리 / SecureStore | API 요청 인증 헤더에 사용 |
| Refresh Token | 30일 | 앱 SecureStore / Keychain | Access Token 만료 시 갱신 |
인증 흐름
1. 로그인
POST /api/v1/auth/login
→ { access_token, refresh_token, expires_in: 3600, user: {...} }
2. API 요청 시 헤더 포함
Authorization: Bearer {access_token}
3. Access Token 만료(1시간) 후 갱신
POST /api/v1/auth/refresh
Body: { "refresh_token": "..." }
→ 새 access_token + 새 refresh_token 발급 (기존 토큰 파기)
4. 로그아웃
POST /api/v1/auth/logout
→ DB의 Refresh Token 파기, 모든 기기 로그아웃
4. 응답 표준 형식
성공 응답
{
"success": true,
"message": "성공 메시지", // 선택
"data": { ... } // 단건: object / 목록: array
}
페이지네이션 응답
{
"success": true,
"data": [ ... ],
"meta": {
"total": 100, // 전체 건수
"page": 1, // 현재 페이지
"per_page": 20, // 페이지당 건수
"last_page": 5, // 마지막 페이지
"has_more": true // 다음 페이지 존재 여부
}
}
에러 응답
{
"success": false,
"message": "에러 메시지",
"code": "UNAUTHORIZED", // 머신리더블 코드
"errors": { // Validation 오류 시만
"login_id": "아이디를 입력하세요.",
"password": "비밀번호를 입력하세요."
}
}
HTTP 상태 코드
| 코드 | 의미 | 발생 상황 |
| 200 | OK | 정상 처리 |
| 201 | Created | 게시글/댓글 작성, 파일 업로드, 회원가입 성공 |
| 400 | Bad Request | 잘못된 요청, 업로드 오류 |
| 401 | Unauthorized | 토큰 없음/만료, 로그인 실패 |
| 403 | Forbidden | 권한 없음, 파일 업로드 비허용 게시판 |
| 404 | Not Found | 게시판/게시글/파일 없음 |
| 422 | Validation Error | 필수 입력값 누락, 형식 오류 |
| 429 | Too Many Requests | Rate Limit 초과 |
| 500 | Internal Server Error | 서버/DB 오류 |
5. 전체 엔드포인트 목록 (22개)
| 메서드 | 엔드포인트 | 설명 | 인증 |
| POST | /api/v1/auth/login | 로그인 → 토큰 발급 | |
| POST | /api/v1/auth/register | 회원가입 → 자동 로그인 | |
| POST | /api/v1/auth/logout | 로그아웃 → 토큰 파기 | 🔒 |
| POST | /api/v1/auth/refresh | Access Token 갱신 | |
| GET | /api/v1/auth/me | 내 정보 조회 | 🔒 |
| GET | /api/v1/boards | 게시판 목록 | |
| GET | /api/v1/boards/:board_key | 게시판 정보 | |
| GET | /api/v1/boards/:board_key/posts | 게시글 목록 (페이지네이션) | 권한별 |
| GET | /api/v1/boards/:board_key/posts/:id | 게시글 상세 + 첨부파일 | 권한별 |
| POST | /api/v1/boards/:board_key/posts | 게시글 작성 (file_ids 포함) | 🔒 |
| PUT | /api/v1/boards/:board_key/posts/:id | 게시글 수정 | 🔒 |
| DELETE | /api/v1/boards/:board_key/posts/:id | 게시글 삭제 (소프트) | 🔒 |
| GET | /api/v1/posts/:post_id/comments | 댓글 목록 | |
| POST | /api/v1/posts/:post_id/comments | 댓글 작성 | 🔒 |
| DELETE | /api/v1/posts/:post_id/comments/:id | 댓글 삭제 | 🔒 |
| GET | /api/v1/members/:login_id | 회원 공개 프로필 | |
| PUT | /api/v1/members/me | 내 정보 수정 | 🔒 |
| PUT | /api/v1/members/me/password | 비밀번호 변경 | 🔒 |
| POST | /api/v1/upload/image | 이미지 업로드 (multipart) | 🔒 |
| POST | /api/v1/upload/file | 파일 업로드 (multipart) | 🔒 |
| POST | /api/v1/upload/attach | 파일-게시글 연결 | 🔒 |
| DELETE | /api/v1/upload/:file_id | 임시 파일 삭제 | 🔒 |
6. 인증 API — /api/v1/auth/*
6-1. 로그인
POST /api/v1/auth/login아이디 또는 이메일로 로그인. 성공 시 Access Token + Refresh Token 발급.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| login_id | Body | 필수 | 아이디 또는 이메일 |
| password | Body | 필수 | 비밀번호 |
Request Body (JSON)
{"login_id": "admin","password": "비밀번호"}
Response
{"success": true,"data": {
"token_type": "Bearer",
"access_token": "eyJ...",
"refresh_token": "a1b2c3...",
"expires_in": 3600,
"user": { "id": "...", "login_id": "admin", "name": "관리자", "role": "admin", "level": "9", "point": "500" }
}}
6-2. 회원가입
POST /api/v1/auth/register신규 회원가입. 성공 시 자동 로그인되어 토큰 즉시 반환 (HTTP 201).
파라미터
| 이름 | 위치 | 필수 | 설명 |
| login_id | Body | 필수 | 아이디 (4자 이상) |
| password | Body | 필수 | 비밀번호 (6자 이상) |
| name | Body | 필수 | 이름/닉네임 |
| Body | 선택 | 이메일 |
Request Body (JSON)
{"login_id": "newuser","password": "pass1234","name": "홍길동","email": "user@email.com"}
Response
{"success": true,"message": "회원가입이 완료되었습니다.","data": { "token_type": "Bearer", "access_token": "...", ... }}
site_settings의 join_level=0 이면 가입 불가 (403)
6-3. 로그아웃
POST /api/v1/auth/logout현재 기기의 Refresh Token 파기. 이후 토큰 갱신 불가.
Response
{"success": true,"message": "로그아웃 되었습니다."}
6-4. Access Token 갱신
POST /api/v1/auth/refreshRefresh Token으로 새 Access Token 발급. 기존 Refresh Token은 즉시 파기되고 새 토큰 발급 (토큰 로테이션).
파라미터
| 이름 | 위치 | 필수 | 설명 |
| refresh_token | Body | 필수 | 로그인 시 발급된 refresh_token |
Request Body (JSON)
{"refresh_token": "a1b2c3..."}
Response
{"success": true,"data": {"token_type": "Bearer","access_token": "eyJ...(새)","refresh_token": "d4e5f6...(새)","expires_in": 3600}}
6-5. 내 정보 조회
GET /api/v1/auth/me현재 로그인한 사용자 정보 반환. 비밀번호 등 민감 정보는 제외.
Response
{"success": true,"data": {"id": "...","login_id": "admin","name": "관리자","email": "admin@example.com","role": "admin","level": "9","point": "500","profile_img": null}}
7. 게시판/게시글 API — /api/v1/boards/*
7-1. 게시판 목록
GET /api/v1/boards활성화된 게시판 전체 목록 반환.
Response
{"success": true,"data": [{"id":"1","board_name":"자유게시판","board_key":"free","board_type":"normal","description":"...","read_level":"0","write_level":"1"},...]}
7-2. 게시글 목록
GET /api/v1/boards/:board_key/posts게시글 목록 반환. 페이지네이션, 검색, 카테고리 필터 지원.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| board_key | URL | 필수 | 게시판 키 (예: free) |
| page | Query | 선택 | 페이지 번호 (기본: 1) |
| per_page | Query | 선택 | 페이지당 건수 (기본: 20, 최대: 100) |
| q | Query | 선택 | 검색어 (제목+내용) |
| category | Query | 선택 | 카테고리 필터 |
Response
{"success": true,"data": [{"id":"...","title":"제목","category":"공지","view_count":"150","like_count":"12","comment_count":"5","thumbnail":"...","author_name":"홍길동","author_id":"hong","excerpt":"내용 미리보기...","created_at":"2026-07-22 09:00:00"}],"meta": {"total": 100,"page": 1,"per_page": 20,"last_page": 5,"has_more": true}}
7-3. 게시글 상세
GET /api/v1/boards/:board_key/posts/:id게시글 상세 + 첨부파일 목록 + 여분필드 반환. 조회수 자동 증가.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| board_key | URL | 필수 | 게시판 키 |
| id | URL | 필수 | 게시글 ID |
Response
{"success": true,"data": {"id":"...","title":"제목","content":"<p>본문HTML</p>","files": [{"id":"1","orig_name":"사진.jpg","file_ext":"jpg","file_size":"102400","url":"https://사이트.com/data/uploads/..."}],"extra_fields": {...},"author_name":"홍길동","created_at":"..."}}
7-4. 게시글 작성
POST /api/v1/boards/:board_key/posts게시글 작성. 업로드된 파일을 file_ids로 연결 가능.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| board_key | URL | 필수 | 게시판 키 |
Request Body (JSON)
{"title": "게시글 제목","content": "<p>본문 HTML</p>","category": "공지","file_ids": [1, 2, 3]}
Response
{"success": true,"message": "게시글이 등록되었습니다.","data": {"id": "1784211424708247","post_id": "1784211424708247"}}
썸네일 자동 생성 (게시판 thumb 설정 적용)
7-5. 게시글 수정
PUT /api/v1/boards/:board_key/posts/:id게시글 수정. 본인 또는 관리자만 가능.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| board_key | URL | 필수 | 게시판 키 |
| id | URL | 필수 | 게시글 ID |
Request Body (JSON)
{"title": "수정된 제목","content": "<p>수정된 내용</p>","category": "일반"}
Response
{"success": true,"message": "게시글이 수정되었습니다.","data": {"id": "..."}}
7-6. 게시글 삭제
DELETE /api/v1/boards/:board_key/posts/:id게시글 소프트 삭제 (status=0). 본인 또는 관리자만 가능.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| board_key | URL | 필수 | 게시판 키 |
| id | URL | 필수 | 게시글 ID |
Response
{"success": true,"message": "게시글이 삭제되었습니다."}
8. 댓글 API — /api/v1/posts/*
8-1. 댓글 목록
GET /api/v1/posts/:post_id/comments댓글 목록. 기본 페이지당 50개.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| post_id | URL | 필수 | 게시글 ID |
| page | Query | 선택 | 페이지 번호 |
| per_page | Query | 선택 | 건수 (기본: 50) |
Response
{"success": true,"data": [{"id":"...","content":"댓글 내용","parent_id":"0","depth":"0","is_secret":"0","author_name":"홍길동","author_id":"hong","created_at":"..."}],"meta": {"total": 30,"page": 1,"per_page": 50}}
8-2. 댓글 작성
POST /api/v1/posts/:post_id/comments댓글 작성. 대댓글 지원.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| post_id | URL | 필수 | 게시글 ID |
Request Body (JSON)
{"content": "댓글 내용","parent_id": 0}
Response
{"success": true,"message": "댓글이 등록되었습니다.","data": {"id": "..."}}
댓글 작성 시 posts.comment_count 자동 증가
8-3. 댓글 삭제
DELETE /api/v1/posts/:post_id/comments/:id댓글 소프트 삭제. 본인 또는 관리자만 가능.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| post_id | URL | 필수 | 게시글 ID |
| id | URL | 필수 | 댓글 ID |
Response
{"success": true,"message": "댓글이 삭제되었습니다."}
9. 회원 API — /api/v1/members/*
9-1. 회원 공개 프로필
GET /api/v1/members/:login_id회원 공개 정보 조회. 비밀번호 등 민감 정보 제외.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| login_id | URL | 필수 | 회원 로그인 아이디 |
Response
{"success": true,"data": {"id":"...","name":"홍길동","login_id":"hong","level":"5","point":"1200","bio":"자기소개","profile_img":null,"join_date":"2026-01-01 00:00:00"}}
9-2. 내 정보 수정
PUT /api/v1/members/me이름, 이메일 수정. 변경할 항목만 포함.
Request Body (JSON)
{"name": "새이름","email": "new@email.com"}
Response
{"success": true,"message": "회원 정보가 수정되었습니다."}
9-3. 비밀번호 변경
PUT /api/v1/members/me/password비밀번호 변경. 변경 성공 시 모든 기기 토큰 파기.
Request Body (JSON)
{"current_password": "현재비밀번호","new_password": "새비밀번호"}
Response
{"success": true,"message": "비밀번호가 변경되었습니다. 다시 로그인하세요."}
new_password 6자 이상 필수
10. 파일 업로드 API — /api/v1/upload/*
2단계 방식으로 동작합니다. 먼저 파일을 업로드(file_id 반환)하고, 게시글 작성 시 file_ids 배열로 연결합니다.
【업로드 흐름】
1단계: 파일 업로드 (post_id=0으로 임시 저장)
POST /api/v1/upload/image 또는 POST /api/v1/upload/file
→ { file_id: 42, url: "https://..." }
2단계: 게시글 작성 시 file_ids 포함 (자동 연결)
POST /api/v1/boards/free/posts
Body: { "title": "...", "content": "...", "file_ids": [42, 43] }
또는 게시글 작성 후 별도 연결
POST /api/v1/upload/attach
Body: { "post_id": "...", "file_ids": [42, 43] }
10-1. 이미지 업로드
POST /api/v1/upload/image이미지 파일만 허용. multipart/form-data 전송.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| file | Form | 필수 | 이미지 파일 (jpg, jpeg, png, gif, webp, bmp) |
| board_key | Form | 필수 | 게시판 키 (업로드 권한 및 썸네일 설정 적용) |
Request Body (JSON)
// multipart/form-data 전송 (JSON 아님)
// React Native FormData 사용
Response
{"success": true,"message": "이미지가 업로드되었습니다.","data": {"file_id": 42,"url": "https://사이트.com/data/uploads/boards/free/20260722_abc123.jpg","save_path": "boards/free/20260722_abc123.jpg","orig_name": "product.jpg","file_size": 102400,"file_ext": "jpg","is_image": true}}
게시판 use_file=0 이면 403 반환
시스템 upload_max_size 초과 시 400 반환
이미지 getimagesize() 검증 — 위장 파일 차단
썸네일 자동 생성 (게시판 thumb 설정 적용)
10-2. 파일 업로드
POST /api/v1/upload/file이미지 포함 모든 파일. 위험 확장자 자동 차단.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| file | Form | 필수 | 업로드할 파일 |
| board_key | Form | 필수 | 게시판 키 |
Request Body (JSON)
// multipart/form-data 전송
Response
{"success": true,"data": {"file_id": 43,"url": "https://...","orig_name": "document.pdf","file_ext": "pdf","is_image": false,...}}
시스템 upload_allowed_ext 설정 자동 적용
10-3. 파일-게시글 연결
POST /api/v1/upload/attach업로드된 임시 파일(post_id=0)을 게시글에 연결.
Request Body (JSON)
{"post_id": "1784211424708247","file_ids": [42, 43]}
Response
{"success": true,"message": "2개 파일이 게시글에 연결되었습니다.","data": {"post_id": "...","attached": 2,"requested": 2}}
본인 게시글 또는 관리자만 가능
10-4. 임시 파일 삭제
DELETE /api/v1/upload/:file_id게시글에 연결되지 않은 임시 파일(post_id=0) 삭제.
파라미터
| 이름 | 위치 | 필수 | 설명 |
| file_id | URL | 필수 | 삭제할 파일 ID |
Response
{"success": true,"message": "파일이 삭제되었습니다."}
물리 파일 + 썸네일 + DB 레코드 모두 삭제
11. 관리자 화면 사용법
관리자 → 사이트 → DX API 또는 /admin/dx-api 접속대시보드
| 항목 | 설명 |
| 활성 토큰 | 현재 유효한 Refresh Token 수 |
| 연결된 기기 | 토큰이 발급된 고유 회원 수 |
| 엔드포인트 | 등록된 API 엔드포인트 수 (22개) |
| 인증 방식 | Bearer Token 방식 표시 |
API 테스터 사용법
1. 게시판 선택 드롭다운에서 테스트할 게시판 선택2. 게시글 ID 입력 (댓글/게시글 상세 테스트 시)
3. 상단 프리셋 버튼 클릭 (로그인, 게시글 목록, 댓글 작성, 파일 업로드 등)
4. 파일 업로드 프리셋 선택 시 파일 선택 필드 자동 표시
5. 전송 버튼 클릭
6. 로그인 성공 시 "access_token 자동입력" 버튼 클릭 → 이후 요청에 자동 포함
토큰 관리
• 활성 토큰 목록: 회원별 발급일시, 만료일시 확인• 개별 파기: 특정 회원의 기기에서 로그아웃 처리
• 전체 파기: 전체 앱 사용자 재로그인 필요 (긴급 보안 조치)
12. 테스트 방법
12-1. 관리자 내장 테스터
/admin/dx-api 에서 브라우저로 바로 테스트 가능. 별도 도구 불필요.12-2. cURL
로그인
curl -X POST https://사이트.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"login_id":"admin","password":"비밀번호"}'
게시글 목록
curl https://사이트.com/api/v1/boards/free/posts
인증 요청
curl https://사이트.com/api/v1/auth/me \
-H "Authorization: Bearer eyJ..."
게시글 작성
curl -X POST https://사이트.com/api/v1/boards/free/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ..." \
-d '{"title":"테스트 글","content":"<p>내용</p>"}'
이미지 업로드 (cURL)
curl -X POST https://사이트.com/api/v1/upload/image \
-H "Authorization: Bearer eyJ..." \
-F "file=@/path/to/image.jpg" \
-F "board_key=free"
12-3. Postman
1. 새 컬렉션 생성: DX API2. 환경 변수 설정: BASE_URL = https://사이트.com, ACCESS_TOKEN = (비어있음)
3. 로그인 요청 Tests 탭에 자동 토큰 저장 스크립트 추가:
const res = pm.response.json();
if (res.success) {
pm.environment.set("ACCESS_TOKEN", res.data.access_token);
pm.environment.set("REFRESH_TOKEN", res.data.refresh_token);
}
4. 이후 모든 요청 Authorization 탭 → Bearer Token → {{ACCESS_TOKEN}}5. 파일 업로드: Body → form-data → file 키에 파일 선택
12-4. React Native
api.js — Axios 클라이언트 설정
import axios from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage";
const api = axios.create({
baseURL: "https://사이트.com/api/v1",
headers: { "Content-Type": "application/json" },
timeout: 10000,
});
// 요청 인터셉터 — 토큰 자동 주입
api.interceptors.request.use(async (config) => {
const token = await AsyncStorage.getItem("access_token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// 응답 인터셉터 — 401 시 토큰 자동 갱신
api.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 401) {
const refresh = await AsyncStorage.getItem("refresh_token");
if (refresh) {
try {
const res = await axios.post(
"https://사이트.com/api/v1/auth/refresh",
{ refresh_token: refresh }
);
if (res.data.success) {
const { access_token, refresh_token } = res.data.data;
await AsyncStorage.setItem("access_token", access_token);
await AsyncStorage.setItem("refresh_token", refresh_token);
error.config.headers.Authorization = `Bearer ${access_token}`;
return axios(error.config);
}
} catch(e) {}
}
}
return Promise.reject(error);
}
);
export default api;
로그인
async function login(loginId, password) {
const res = await api.post("/auth/login", { login_id: loginId, password });
const { access_token, refresh_token, user } = res.data.data;
await AsyncStorage.setItem("access_token", access_token);
await AsyncStorage.setItem("refresh_token", refresh_token);
return user;
}
이미지 업로드 (React Native)
async function uploadImage(uri, boardKey) {
const form = new FormData();
form.append("file", {
uri,
type: "image/jpeg",
name: "photo.jpg",
});
form.append("board_key", boardKey);
const res = await api.post("/upload/image", form, {
headers: { "Content-Type": "multipart/form-data" },
});
return res.data.data.file_id; // file_id 반환
}
// 파일 업로드 후 게시글 작성
async function createPost(boardKey, title, content, fileIds) {
const res = await api.post(`/boards/${boardKey}/posts`, {
title,
content,
file_ids: fileIds,
});
return res.data.data.post_id;
}
13. 다른 플러그인에서 API 확장하기
dx_api_endpoints 훅으로 어떤 플러그인에서도 새 엔드포인트를 추가할 수 있습니다.
// plugins/my-shop/plugin.php
dx_add_hook("dx_api_endpoints", function() {
// GET /api/v1/shop/products
DxApiRouter::get("/v1/shop/products", function($params) {
$db = Database::getInstance();
$pg = DxApiRequest::pagination(20);
$limit = (int)$pg["per_page"];
$offset = (int)$pg["offset"];
$items = $db->rows(
"SELECT * FROM `{$db->table('products')}` WHERE status=1
ORDER BY id DESC LIMIT {$limit} OFFSET {$offset}"
);
$total = (int)$db->value(
"SELECT COUNT(*) FROM `{$db->table('products')}` WHERE status=1"
);
DxApiResponse::paginate($items, $total, $pg["page"], $pg["per_page"]);
});
// POST /api/v1/shop/orders (인증 필요)
DxApiRouter::post("/v1/shop/orders", function($params) {
$payload = isset($GLOBALS["dx_api_user"]) ? $GLOBALS["dx_api_user"] : array();
$productId = DxApiRequest::int("product_id");
// ... 주문 처리
DxApiResponse::ok(array("order_id" => $orderId), "주문 완료", 201);
}, array("auth"));
});
14. 오류 코드 목록
| code | HTTP | 설명 | 해결 방법 |
| LOGIN_FAILED | 401 | 아이디/비밀번호 불일치 | 자격증명 확인 |
| UNAUTHORIZED | 401 | 토큰 없음 또는 만료 | 토큰 갱신 또는 재로그인 |
| INVALID_REFRESH_TOKEN | 401 | 만료/사용된 Refresh Token | 재로그인 필요 |
| FORBIDDEN | 403 | 접근 권한 없음 | 권한 레벨 또는 소유자 확인 |
| NOT_FOUND | 404 | 리소스 없음 | URL/ID 확인 |
| VALIDATION_ERROR | 422 | 입력값 오류 | errors 필드 확인 |
| TOO_MANY_REQUESTS | 429 | Rate Limit 초과 | 잠시 후 재시도 |
| UPLOAD_ERROR | 400 | 파일 업로드 오류 | 파일 선택 여부 확인 |
| FILE_TOO_LARGE | 400 | 파일 크기 초과 | upload_max_size 설정 확인 |
| FILE_NOT_ALLOWED | 400 | 허용되지 않는 확장자 | 허용 확장자 확인 |
| INVALID_IMAGE | 400 | 유효하지 않은 이미지 | 실제 이미지 파일인지 확인 |
| UPLOAD_NOT_ALLOWED | 403 | 게시판 파일 업로드 비허용 | 게시판 use_file 설정 확인 |
| UPLOAD_FAILED | 500 | 서버 파일 저장 실패 | data/uploads/ 권한 확인 |
| INTERNAL_SERVER_ERROR | 500 | 서버 오류 | 서버 로그 확인 |
| PLUGIN_NOT_FOUND | 404 | dx-api 플러그인 미설치 | v1.php 복사 여부 확인 |
| PLUGIN_DISABLED | 503 | dx-api 비활성화 | 관리자 → 플러그인 활성화 |
15. 보안 권장 사항
필수
• HTTPS 필수 — HTTP에서는 Bearer Token이 평문 전송됨• Access Token은 앱 메모리에만 저장
• Refresh Token은 AsyncStorage 대신 Expo SecureStore / iOS Keychain / Android Keystore 사용
• data/uploads/ 폴더 권한 755 설정
권장
• 프로덕션에서 CORS origin을 * 대신 실제 도메인으로 제한 (Response.php 수정)• 관리자 화면에서 주기적으로 만료 토큰 정리
• 의심스러운 활동 시 관리자 화면 → 전체 토큰 파기
• API 불필요 시 플러그인 비활성화로 완전 차단
CORS 도메인 제한 (Response.php)
// plugins/dx-api/core/Response.php send() 메서드에서
// 변경 전:
header("Access-Control-Allow-Origin: *");
// 변경 후:
$allowed = array("https://myapp.com", "https://admin.myapp.com");
$origin = isset($_SERVER["HTTP_ORIGIN"]) ? $_SERVER["HTTP_ORIGIN"] : "";
if (in_array($origin, $allowed)) {
header("Access-Control-Allow-Origin: " . $origin);
}