<aside> ❗ API 사용 전 반드시 정독해주세요
</aside>
이 문서는 Toduck API에 대한 전반적 이해를 돕는 것을 목적으로 합니다.
<aside> ❗ Toduck API의 일부는 사용하려면 엑세스 토큰이 필요합니다. 토큰은 로그인 관련 API를 통해 받을 수 있습니다.
</aside>
모든 API 요청을 전송하는 기본 URL은 https://api.toduck.app
입니다. 모든 API 요청에는 HTTPS가 필요합니다.
Toduck API는 가능한 한 RESTful 규칙을 따릅니다. 요청과 응답 본문은 JSON으로 인코딩됩니다.
<aside> 💡 개발용 서버 URL은 https://dev-api-toduck.seol.pro/입니다. 현재 개발용 URL만 사용 가능합니다. 개발용 API 에서는 소셜 로그인 등 일부 API가 동작하지 않을 수 있습니다.
</aside>
<aside> ❗ 백엔드 API는 문자열의 트림(trim) 및 글자 수 제한과 같은 기본적인 데이터 처리 및 검증을 수행합니다. 그러나 사용자 경험 향상과 데이터 무결성을 보장하기 위해 클라이언트에서도 동일한 검증을 수행하는 것이 중요합니다.
</aside>
엔드포인트 구조
모든 엔드포인트는 아래 구조를 따릅니다:
{기본 URL}/{API 버전}/{리소스 경로}
https://api.toduck.app/v1/resources
https://api.toduck.app
/v1
/resources
JSON 규칙
Camel Case
로 되어 있습니다.2024-05-23 21:41:51
)을 포함하며, 날짜는 날짜(2020-08-12
)만 포함합니다.""
은 null로 처리됩니다.time
필드가 null인 경우에는 종일 루틴으로 간주함QueryParameter, Path Variable 규칙
""
은 null로 처리됩니다.<aside> ❗ 예외를 포함한 모든 응답은 공통 응답 형식을 준수합니다. 공통 응답 형식을 준수하지 않는 엔드포인트가 있는 경우 백엔드 담당자에게 알려주세요.
</aside>
모든 API 응답은 다음과 같은 구조를 가집니다.
{
"code": 20000 ~ 50001,
"content": {},
"message": "에러 메세지"
}
종류 | code |
content |
message |
비고 | 예시 |
---|---|---|---|---|---|
성공 | 20000 |
응답 내용 Object | null |
성공 응답 | { |
“code”: 20000,
“content”: {
“email”: “[email protected]”,
“role”: “ADMIN”
},
message: null
} |
| 일반 실패 | 40000
~ 49999
| null
| 실패 사유 (디버깅 용도) | 해당 코드를 통해 에러 처리를 수행 | {
“code”: 4XXXX,
“content”: null,
message: “상세 사유”
} |
| Validation 실패 | 30001
| 실패 사유 Object (디버깅용) | “잘못된 요청 형식입니다.”
| 클라이언트측에서 적절한 Validation 이후 응답을 보내도록 수정 필요 | {
“code”: 30001,
"content": {
"email": "이메일 형식이 아닙니다."
},
"message": "잘못된 요청 형식입니다."
} |
| Validation 실패 | 30002
| 실패 사유 (디버깅용) | “데이터 형식이 유효하지 않습니다.”
| 클라이언트측에서 적절한 Validation 이후 응답을 보내도록 수정 필요 | {
“code”: 30002,
"content": “상세 사유”,
"message": “데이터 형식이 유효하지 않습니다.”
} |
| 서버 예외 | 50001
| null
| “서버 오류입니다.”
| 백엔드 결함, 담당자에게 문의
| {
“code”: 50001,
"content": null,
"message": “서버 오류입니다.”
} |
특정 목록을 조회하는 엔드포인트는 커서 기반 페이지네이션 요청을 지원합니다. 보다 자세한 설명은 각 엔드포인트별 API 명세를 확인하세요.
요청에 대한 매개변수
파라미터 | 유형 | 기본값 | 설명 |
---|---|---|---|
cursor |
number |
목록의 시작 | 페이징을 위한 커서 값으로, 이 값 이후의 데이터부터 가져옵니다. 생략시 첫 페이지를 가져옵니다. |
limit |
number |
엔드포인트별 명세 참고 | 한 페이지에 반환할 항목의 최대 개수입니다. 만약 이 값이 제공되지 않으면 기본값이 사용됩니다. |
기본 응답 형식
필드 | 유형 | 설명 |
---|---|---|
hasMore |
boolean |
응답에 목록의 끝이 포함되는지를 구분합니다. 더 이상 결과가 없으면 false , 그렇지 않으면 true 를 반환합니다. |
nextCursor |
number |
after 파라미터로 값을 같은 엔드포인트에 전달하여 결과의 다음 페이지를 검색하는 데 사용할 수 있는 고유값으로, hasMore 가 true 일 때만 사용할 수 있습니다. |
results |
array of objects |
엔드포인트별 결과의 전체 목록 또는 일부 목록입니다. 엔드포인트별 필드 명은 상이할 수 있습니다. 자세한 내용은 각 API 별 상세 명세를 참고하세요. |
일반 실패 | 40000 ~ 49999 |
null |
실패 사유 (디버깅 용도) | 해당 코드를 통해 에러 처리를 수행 | { “code”: 4XXXX, “content”: null, message: “상세 사유”} |
---|
공통 응답 형식에서 일반 실패
상황이 발생하는 경우, 아래 에러코드가 응답에 포함됩니다.
에러코드 앞의 세자리는 관련 도메인을 나타냅니다.
도메인 | 에러코드 |
---|---|
auth | 401XX |
user | 402XX |
schedule | 431XX |
routine | 432XX |
도메인 | 에러코드 |
---|---|
social | 404XX |
diary | 405XX |
focus | 406XX |
기타 | 499xx |
우리 프로젝트는 API 엔드포인트 명세와 에러코드 목록을 체계적으로 관리합니다. 이를 코드베이스에 통합하여 관리하고, 사용자 친화적인 인터페이스를 통해 클라이언트 개발자들에게 제공합니다.
<aside> ❗ 문서에는 실제 사용 가능한 엔드포인트에 대한 명세와 에러코드 목록을 제공합니다. 정상적으로 동작하지 않거나 추가 설명이 필요한 경우 담당자에게 문의하세요.
</aside>
개발서버용
API 명세서 화면
API 명세서는 Swagger UI를 기반으로 합니다.
Swagger UI는 다음과 같은 특징을 가집니다.
에러코드 명세서 화면
에러코드 명세서에서는 애플리케이션에서 발생 가능한 모든 에러코드에 대한 정보를 제공합니다. 각 코드는 도메인에 맞게 분류되어 있으며, Http 상태 코드, 에러 코드, 메세지, 그리고 추가 설명을 포함합니다.
원본 코드 보기
를 누르면, 해당 UI를 구성하는 코드를 직접 확인할 수 있습니다.
**JPG("jpg", "image/jpeg"),**
**JPEG("jpeg", "image/jpeg"),**
PNG("png", "image/png"),
GIF("gif", "image/gif"),
BMP("bmp", "image/bmp"),
WEBP("webp", "image/webp");
import UIKit
/// 서버에서 presignedUrl을 발급받는 함수 (예: POST /v1/presigned)
/// - Parameter fileName: 업로드하려는 파일 이름 (ex: "test.jpg")
/// - Parameter completion: 성공 시 presignedUrl을, 실패 시 nil을 반환
func requestPresignedUrl(fileName: String, completion: @escaping (String?) -> Void) {
// 1) API Endpoint
guard let url = URL(string: "<https://api.toduck.app/v1/presigned>") else {
completion(nil)
return
}
// 2) 요청 바디 구성
// 서버에서 기대하는 JSON 구조에 맞춰주세요.
// 예: { "fileNameDtos": [ { "fileName": "test.jpg" } ] }
let bodyJson: [String: Any] = [
"fileNameDtos": [
["fileName": fileName]
]
]
// JSON 직렬화
guard let httpBody = try? JSONSerialization.data(withJSONObject: bodyJson) else {
completion(nil)
return
}
// 3) URLRequest 설정
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = httpBody
// 4) URLSession
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// 에러 체크
if let error = error {
print("requestPresignedUrl error: \\(error)")
completion(nil)
return
}
// HTTP 상태코드 체크
if let httpResponse = response as? HTTPURLResponse,
!(200...299).contains(httpResponse.statusCode) {
print("requestPresignedUrl status code: \\(httpResponse.statusCode)")
completion(nil)
return
}
// 응답 바디 파싱
guard let data = data else {
completion(nil)
return
}
// 서버가 ApiResponse 형태로 준다고 가정
// {"code": 20000, "content": {"fileUrlsDtos":[{"presignedUrl": "...", "fileUrl": "..."}]}, "message": null}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let content = json["content"] as? [String: Any],
let fileUrlsDtos = content["fileUrlsDtos"] as? [[String: Any]],
let firstDto = fileUrlsDtos.first,
let presignedUrl = firstDto["presignedUrl"] as? String {
// Presigned URL 추출 성공
completion(presignedUrl)
} else {
// JSON 구조가 예상과 다름
completion(nil)
}
} catch {
print("JSON parse error: \\(error)")
completion(nil)
}
}
task.resume()
}
import UIKit
/// 서버에서 발급받은 presignedUrl로 이미지를 업로드
/// - Parameter presignedUrl: PUT 요청할 URL (서명 포함)
/// - Parameter image: 업로드할 UIImage
func uploadImageToS3(presignedUrl: String, image: UIImage) {
// 1) 이미지를 JPEG data로 변환
guard let url = URL(string: presignedUrl),
let imageData = image.jpegData(compressionQuality: 0.9) else {
print("잘못된 presignedUrl 또는 UI 이미지를 변환할 수 없습니다.")
return
}
// 2) URLRequest 구성
var request = URLRequest(url: url)
request.httpMethod = "PUT"
// Content-Type: 서버에서 "image/jpeg"로 presignedUrl을 생성했다면
// 여기도 동일하게 "image/jpeg"로 맞춰야 함
request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
// 3) 업로드 (URLSession.uploadTask)
let uploadTask = URLSession.shared.uploadTask(with: request, from: imageData) { data, response, error in
// 에러 체크
if let error = error {
print("Upload failed: \\(error)")
return
}
// HTTP 상태코드 체크
if let httpResponse = response as? HTTPURLResponse {
if (200...299).contains(httpResponse.statusCode) {
print("업로드 성공")
} else {
print("업로드 실패. statusCode = \\(httpResponse.statusCode)")
}
}
}
// 4) 시작
uploadTask.resume()
}
func exampleFlow() {
let fileName = "test.jpg" // 업로드하려는 파일 이름
let image = UIImage(named: "testImage")! // 업로드할 UIImage (예시)
// 1) 서버에서 presignedUrl 발급
requestPresignedUrl(fileName: fileName) { presignedUrl in
guard let presignedUrl = presignedUrl else {
print("Failed to get presignedUrl.")
return
}
print("presignedUrl: \\(presignedUrl)")
// 2) presignedUrl을 사용해 업로드
uploadImageToS3(presignedUrl: presignedUrl, image: image)
}
}