<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 버전}/{리소스 경로}

JSON 규칙

QueryParameter, Path Variable 규칙

공통 응답 형식

<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 파라미터로 값을 같은 엔드포인트에 전달하여 결과의 다음 페이지를 검색하는 데 사용할 수 있는 고유값으로, hasMoretrue일 때만 사용할 수 있습니다.
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 및 에러코드 명세

우리 프로젝트는 API 엔드포인트 명세와 에러코드 목록을 체계적으로 관리합니다. 이를 코드베이스에 통합하여 관리하고, 사용자 친화적인 인터페이스를 통해 클라이언트 개발자들에게 제공합니다.

<aside> ❗ 문서에는 실제 사용 가능한 엔드포인트에 대한 명세와 에러코드 목록을 제공합니다. 정상적으로 동작하지 않거나 추가 설명이 필요한 경우 담당자에게 문의하세요.

</aside>

API 명세서

개발서버용

Toduck Developers

시작하기

API 명세서 화면

API 명세서 화면

API 명세서는 Swagger UI를 기반으로 합니다.

Swagger UI는 다음과 같은 특징을 가집니다.

Swagger UI 상세 사용법

image.png

에러코드 명세서 (배포 이후 사용 가능)

시작하기

에러코드 명세서 화면

에러코드 명세서 화면

에러코드 명세서에서는 애플리케이션에서 발생 가능한 모든 에러코드에 대한 정보를 제공합니다. 각 코드는 도메인에 맞게 분류되어 있으며, Http 상태 코드, 에러 코드, 메세지, 그리고 추가 설명을 포함합니다.

원본 코드 보기 를 누르면, 해당 UI를 구성하는 코드를 직접 확인할 수 있습니다.

Presigned Url

0. 확장자별 contentType

	**JPG("jpg", "image/jpeg"),**
	**JPEG("jpeg", "image/jpeg"),**
	PNG("png", "image/png"),
	GIF("gif", "image/gif"),
	BMP("bmp", "image/bmp"),
	WEBP("webp", "image/webp");

1. PresignedUrl 발급

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()
}

2. 발급받은 presignedUrl로 S3에 이미지 업로드

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()
}

3. 전체 시나리오 예시

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)
    }
}