# Solve Developer Center URL: https://developers.solve.im/docs > 쏠브와 외부 시스템을 연동하는 개발자를 위한 문서입니다. 쏠브와 외부 시스템을 연동하는 개발자를 위한 문서입니다. # AI로 구현하기 URL: https://developers.solve.im/docs/partner-integration/ai-guide > Claude·ChatGPT·Cursor 같은 AI 코딩 도구에 붙여넣으면 규약대로 구현하도록 만든 통합 프롬프트와 AI용 문서 경로입니다. AI 코딩 도구로 연동을 구현한다면 이 페이지부터 쓰세요. 아래 **통합 프롬프트** 하나에 연동 규약, 틀리기 쉬운 점, 완료 기준을 모두 담았습니다. 문서를 일일이 읽히지 않아도 AI 가 같은 기준으로 구현합니다. ## 사용 방법 ### 프롬프트 복사 아래 코드 블록 오른쪽 위의 복사 버튼을 누릅니다. ### 맨 위 "우리 환경" 채우기 언어·프레임워크, 로그인 방식(ⓐ/ⓑ), 회원·구매 테이블 이름을 적습니다. 모르는 칸은 비워 두면 AI 가 먼저 물어봅니다. ### AI 도구에 붙여넣기 Claude Code·Cursor·Codex 처럼 저장소를 읽는 도구라면 저장소 루트에서 실행하세요. AI 가 기존 회원·주문 코드를 찾아 거기에 맞춰 구현합니다. ## 통합 프롬프트 ```text title="solve-integration-prompt.txt" # 과제: 쏠브(Solve) 제휴사 연동 구현 ## 우리 환경 (채워서 사용) - 언어/프레임워크: - 로그인 방식: ⓐ 자격증명 API / ⓑ 호스팅 로그인 페이지 (소셜 로그인이 하나라도 있으면 ⓑ) - 회원 테이블과 불변 회원번호 컬럼: - 구매(주문) 테이블과 "현재 이용 가능" 판정 기준(환불·만료 제외): ## 배경 쏠브는 전자책 학습 앱이다. 우리 사이트에서 책을 산 회원이 쏠브 앱에서 우리 계정으로 로그인하면, 쏠브 서버가 우리 API 를 호출해 구매 목록을 받아 책장에 책을 넣는다. **쏠브가 호출하는 쪽이고, 우리가 API 를 구현하는 쪽**이다. 전체 규약 원문: https://developers.solve.im/llms-full.txt (필요하면 읽어라) ## 구현할 것 1. 로그인 (둘 중 하나) ⓐ 자격증명 로그인 API — POST, JSON 요청 {"id": string, "password": string} - 성공: 200 {"user_key": string} - 아이디·비밀번호 불일치: 4xx (쏠브 앱이 "비밀번호 불일치"로 안내, 재시도 안 함) - 서버 오류: 5xx (쏠브가 최대 2번 재시도) ⓑ 호스팅 로그인 페이지 — 쏠브 앱 웹뷰가 우리 "쏠브 전용 로그인 URL"을 쿼리 파라미터 없이 연다 - 회원이 로그인에 성공하면 우리 **서버에서** 쏠브 콜백 호출: POST {SOLVE_CALLBACK_URL} JSON {"id": string, "platform_type": number} 헤더 Authorization: Bearer {SOLVE_CALLBACK_SECRET} (쏠브 발급, 401 이면 재시도 금지) - 응답 200 {"redirect_url": string} → 같은 웹뷰에서 그 URL 로 302 이동 (새 창·외부 브라우저 금지, URL 수정 금지 — 서명 포함) - 소셜 로그인으로 외부에 다녀오는 경우 세션에 "쏠브 연동 중" 표시를 유지했다가 완료 시 콜백 - SOLVE_CALLBACK_URL, SOLVE_PLATFORM_TYPE, SOLVE_CALLBACK_SECRET 은 쏠브가 전달하는 값 → 환경변수로 2. 구매내역 API — POST, JSON 요청 {"user_key": string} - 200 {"item_list": [{"item_code": string, "item_name": string(선택)}]} - 선택 필드 (v1.1, 없어도 된다 — v1.0 대로 구현한 응답은 수정 없이 유효): 항목마다 price · paid_at · valid_from · valid_until 을 더 담을 수 있다 · price: 0 이상 정수(숫자) 또는 숫자만으로 된 문자열(1~12자리), 원 단위 · paid_at: ISO 8601. 오프셋(Z, +09:00)이 있으면 그대로, 없으면 한국 시간(+09:00), 날짜만이면 그날 00:00 한국 시간 · valid_from, valid_until: 1~32자 문자열 (쏠브는 해석하지 않고 그대로 기록) · 형식이 틀리면 그 필드만 무시한다 (책 지급에는 영향 없음) · price 를 안 보내거나 0 이면 파트너 어드민에 등록한 가격으로 정산한다 · valid_until 이 지나도 책은 자동으로 빠지지 않는다 — 회수는 목록에서 빼는 것뿐이다 - **현재 이용 가능한 구매만** 담는다. 환불·만료는 뺀다 - 구매가 없거나 회원이 없으면 200 {"item_list": []} - 조회 실패·장애는 반드시 5xx. 예외를 삼키고 200 을 보내지 마라 (일부만 조회된 목록이면 빠진 책이 회수된다) - 페이지네이션 없이 한 번에 전부 3. 공통 - HTTPS, Content-Type: application/json, 10초 안에 응답 (쏠브 타임아웃 10초) - 쏠브는 우리가 발급한 시크릿을 헤더에 담아 보낸다. 헤더 이름은 우리가 정한다 (기본값: X-Solve-Secret) - 시크릿은 환경변수로 두고(비어 있으면 서버 시작 실패) 상수 시간 비교로 확인, 불일치면 `401` 또는 `403` - 쏠브는 `401` 과 `403` 을 구분하지 않고 같은 인증 실패로 처리한다 (둘 다 재시도 없음) - 같은 요청이 재시도로 여러 번 와도 데이터가 바뀌면 안 된다 (조회 전용) - 응답 본문은 1MB(1,048,576바이트) 이하. 쏠브는 1MB 를 넘는 응답을 받지 않는다 (넘으면 그 호출은 실패, 재시도 없음 — 구매내역이면 기존 책장 유지). 구매내역·ⓐ 로그인 모두 해당, 필요한 필드만 담는다 ## 반드시 지킬 것 (틀리기 쉬움) - user_key(ⓑ 의 id)는 **절대 바뀌지 않는 내부 회원번호**. 로그인 아이디·이메일·소셜 계정 ID 금지 - 탈퇴 회원의 user_key 를 다른 회원에게 재사용 금지 - item_code 는 상품 단위 코드이며, 쏠브 파트너 어드민에 입력한 "연동코드"와 같아야 한다. 쏠브는 item_code 의 앞뒤 공백을 떼고 비교하고, 그 밖은 대소문자까지 글자 그대로 같아야 한다 (연동코드 앞뒤에 공백을 넣지 않는다) - 비밀번호·시크릿을 로그에 남기지 않는다 - 예외를 삼키고 200 을 보내지 않는다 ## 결정 기본값 (모호하면 이렇게) - 엔드포인트 경로: POST /solve/login (ⓐ), POST /solve/purchases - 아이디·비밀번호 불일치 401, 시크릿 불일치 `401` 또는 `403` (기본은 403), 요청 형식 오류 400, 그 외 예외 500 - 에러 응답 본문: {"error": ""} ## 산출물 1. 위 API 구현 코드 (기존 회원·주문 코드를 찾아서 재사용) 2. 테스트: 구매 회원 200, 없는 회원 200+빈 배열, 환불 상품 제외, 틀린 시크릿 `401` 또는 `403`, DB 오류 500 (ⓐ 는 맞는/틀린 비밀번호도) 3. 쏠브가 보낼 요청을 재현하는 curl 명령 4. 쏠브에 전달할 값 목록: 각 URL, 시크릿 헤더 이름, 테스트 계정 5. 구현 뒤 `solve-selfcheck.mjs`(https://developers.solve.im/tools/solve-selfcheck.mjs)로 확인한다 — `--init` 으로 설정 틀을 만들고 값을 채워 실행하며, FAIL 이 0개일 때까지 고친다 (ⓑ 는 `solve-callback-mock.mjs` 로 콜백을 시험) 작업 전에 우리 코드베이스에서 회원·구매 관련 코드를 먼저 찾아보고, 구현 계획을 짧게 보여준 뒤 진행해라. ``` ## 저장소에 규칙으로 넣기 같은 저장소에서 AI 에게 여러 번 일을 시킨다면, 프롬프트를 규칙 파일로 넣어 두면 매번 붙여넣지 않아도 됩니다. | 도구 | 파일 위치 | | -------------- | ------------------------------------- | | Claude Code | 저장소 루트 `CLAUDE.md` 에 추가 | | Codex, 기타 에이전트 | 저장소 루트 `AGENTS.md` 에 추가 | | Cursor | `.cursor/rules/solve-integration.mdc` | | GitHub Copilot | `.github/copilot-instructions.md` | ## AI 도구용 문서 경로 | 경로 | 내용 | 쓰는 곳 | | ------------------------------------------------------------- | ------------- | --------------------------------------------------------------------- | | [`/llms.txt`](https://developers.solve.im/llms.txt) | 전체 문서 목차 | AI 에게 "여기서 필요한 문서를 찾아 읽어" | | [`/llms-full.txt`](https://developers.solve.im/llms-full.txt) | 전체 문서를 한 파일로 | 한 번에 모든 규약을 넘길 때 | | 각 페이지 URL 뒤에 `.md` | 그 페이지만 마크다운으로 | 특정 페이지만 넘길 때 (예: `/docs/partner-integration/purchase-history-api.md`) | 각 페이지 제목 아래의 **마크다운 복사**·**AI로 열기** 버튼으로도 그 페이지를 바로 넘길 수 있습니다. 특히 "예외를 삼키고 200 응답"과 "로그인 아이디를 user\_key 로 사용"은 AI 가 자주 만드는 실수입니다. [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding)의 셀프 점검 도구(`solve-selfcheck.mjs`)로 확인한 뒤 쏠브에 전달해 주세요. # 변경 이력 URL: https://developers.solve.im/docs/partner-integration/changelog > 제휴사 연동 API 버전 이력 # 변경 이력 ## 2026-10-03 — 응답 크기 상한 명시 `contractVersion` 은 그대로입니다. 구매내역 API 와 ⓐ 자격증명 로그인 API 의 응답 본문 크기 상한을 문서에 적었습니다. * **응답은 1MB 이하**: 응답 본문은 1MB(1,048,576바이트) 이하여야 합니다. 쏠브는 1MB 를 넘는 응답을 받지 않으며, 넘으면 그 호출은 실패로 처리하고 재시도하지 않습니다. 구매내역은 기존 책장을 유지합니다. ([구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api), [ⓐ 자격증명 로그인 API](https://developers.solve.im/docs/partner-integration/login-credential-api)) * **`item_code` 비교 설명 정정**: 쏠브는 `item_code` 의 앞뒤 공백을 떼고 비교합니다. 그 밖은 대소문자까지 글자 그대로 같아야 합니다. ([상품 연결](https://developers.solve.im/docs/partner-integration/product-catalog)) ## 2026-10-02 — 점검 도구 추가 규약은 바뀌지 않았습니다. 제휴사가 자기 PC 에서 돌리는 점검 도구를 추가했습니다. * **셀프 점검 v2 (`solve-selfcheck.mjs`)**: 구매내역 API 와 ⓐ 로그인 API 를 한 번에 점검합니다. 응답 형식, 선택 필드, 오류 응답, 응답 시간을 확인합니다. 기존 bash 스크립트는 "빠른 확인"으로 그대로 남아 있습니다. ([테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding)) * **콜백 흉내 도구 (`solve-callback-mock.mjs`)**: ⓑ 방식 제휴사가 계약 전에도 자기 로그인 페이지의 콜백을 시험할 수 있습니다. ([ⓑ 호스팅 로그인 페이지](https://developers.solve.im/docs/partner-integration/login-hosted-page)) ## v1.1.0 — 2026-10-02 v1.0.0 과 호환됩니다. **기존 구현은 수정 없이 동작합니다.** * **구매내역 선택 필드 4개**: 항목마다 `price`·`paid_at`·`valid_from`·`valid_until` 을 선택으로 더 담을 수 있습니다. 없어도 동작하고, 형식이 틀리면 그 필드만 무시하며 책 지급에는 영향이 없습니다. ([구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api)) * **`price` 정산 규칙**: `price` 를 안 보내거나 `0` 이면 파트너 어드민에 등록한 가격으로 정산합니다. * **인증 실패 상태 코드 표기 통일**: 시크릿 불일치는 `401` 또는 `403` 으로 표기합니다. 쏠브는 둘을 구분하지 않고 같은 인증 실패로 처리합니다(둘 다 재시도하지 않습니다). 예시 코드는 그대로 유효합니다. ([구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api), [ⓐ 자격증명 로그인 API](https://developers.solve.im/docs/partner-integration/login-credential-api)) * **정정 — 시크릿 회전**: 제휴사가 발급한 API 시크릿은 제휴사가 새 값과 옛 값을 함께 받아 주는 동안 쏠브가 새 값으로 바꿉니다(반영까지 최대 5분). 쏠브가 발급한 콜백 인증 시크릿은 쏠브가 일정 기간 새 값과 옛 값을 함께 받습니다. ([보안 요구사항](https://developers.solve.im/docs/partner-integration/security)) * **정정 — 롤백**: 연동을 끄면 신규 연동과 동기화가 멈추고, 이미 들어간 책은 남습니다. ([테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding)) * **기존 전자책 연동코드**: 이미 등록한 전자책은 파트너 어드민 상품 상세에서 연동코드를 한 번 넣을 수 있습니다. ([상품 연결](https://developers.solve.im/docs/partner-integration/product-catalog)) * **정정 — 구매 후 사용자 안내**: 앱의 연동 화면으로 바로 들어가는 링크는 제공하지 않습니다. 대신 쏠브북스 웹의 제휴사 연동 화면 주소를 안내합니다. ([구매 후 사용자 안내](https://developers.solve.im/docs/partner-integration/user-discovery)) ## 2026-09-30 — ⓑ 콜백 인증 추가 * **ⓑ 콜백 인증**: 쏠브가 제휴사별로 발급한 시크릿을 `Authorization: Bearer` 헤더로 보내야 합니다. **신규 제휴사는 처음부터 필수**이며, 이미 운영 중인 제휴사는 쏠브와 일정을 맞춰 전환합니다(전환 전까지는 기존처럼 동작). 인증 실패는 `401` 입니다. ([ⓑ 호스팅 로그인 페이지](https://developers.solve.im/docs/partner-integration/login-hosted-page)) * **`redirect_url` 서명**: 쏠브가 `redirect_url` 에 위변조 방지 서명 파라미터를 추가합니다. 제휴사는 지금처럼 URL 을 수정 없이 그대로 사용하면 됩니다. ## 2026-09-30 — 문서 개편 API 계약 변경은 없습니다. 읽기 쉽게 구조를 바꾸고 예시를 늘렸습니다. * **새 페이지**: [구현 순서](https://developers.solve.im/docs/partner-integration/quickstart), [AI로 구현하기](https://developers.solve.im/docs/partner-integration/ai-guide)(통합 프롬프트), [자주 묻는 질문](https://developers.solve.im/docs/partner-integration/faq) * **구현 예시**: 로그인·구매내역 API 페이지에 Node.js·Python·Java·PHP 예시와 curl 재현 명령을 추가했습니다. * **셀프 점검**: [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding)에 구매내역 API 점검 스크립트를 추가했습니다. * **호출 조건 명시**: 호출당 타임아웃 10초, `5xx`·타임아웃 시 최대 2번 재시도, `4xx` 는 재시도하지 않음. * **상황별 응답 표**: 구매내역 API 의 응답별 쏠브 처리를 한 표로 정리했습니다. * **정정 — 빈 목록 응답**: 07-21 에 "빈 `item_list`(200)면 권한을 회수한다"고 적었으나, 실제로는 목록이 **통째로 비면 회수하지 않습니다**(장애 오탐 방지). 일부 항목이 빠진 경우에만 그 항목을 회수합니다. [구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api)의 상황별 응답 표를 확인해 주세요. * **로그인 방식 기준**: 자체 ID/PW 와 소셜 로그인을 함께 운영하면 ⓑ 를 선택하도록 명시했습니다. * **상품 연결**: 파트너 어드민에 전자책을 등록하며 연동코드를 입력하면 카탈로그 파일이 필요 없음을 명시하고, 판매유형(`연동형`·`연동+쏠브북스`)과 패키지 입력 방법을 정리했습니다. * AI 도구용 경로(`/llms.txt`, `/llms-full.txt`, 페이지 URL + `.md`)와 페이지별 "마크다운 복사"·"AI로 열기" 버튼을 제공합니다. ## 2026-07-21 — 문서 정정 쏠브의 실제 동작에 맞게 서술을 바로잡았습니다. API 동작 자체가 바뀐 것은 아니지만, 아래 항목은 구현에 영향을 줄 수 있으니 확인해 주세요. * **`user_key` 미존재 응답** — 회원이 더 이상 존재하지 않으면 빈 `item_list`(200)로 응답해야 권한이 회수됩니다. `404` 같은 오류 응답은 일시 장애로 간주해 기존 권한을 유지합니다. ([구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api)) * **시크릿 발급 주체** — server-to-server 시크릿은 제휴사가 발급하고, 쏠브가 헤더에 담아 보냅니다. * **ⓑ 콜백 `id`** — 소셜 인증을 쓰더라도 소셜 프로필 ID·이메일이 아니라 제휴사 내부 회원의 불변 식별자를 보내야 합니다. * **ⓑ 리다이렉트** — 연동을 시작한 브라우저(쏠브 앱이 연 웹뷰) 안에서 `redirect_url` 로 이동해야 연동이 완료됩니다. ([보안](https://developers.solve.im/docs/partner-integration/security)) * **동기화 시점** — 연동 직후 1회 자동으로 동기화합니다. * **상품 카탈로그** — 파트너 어드민 등록 시 자동 매핑되는 방식과 `item_code` 재사용 금지 규칙을 추가했습니다. * [구매 후 사용자 안내](https://developers.solve.im/docs/partner-integration/user-discovery) 페이지를 새로 만들었습니다. ## 2026-05-29 — 문서 보강 * `item_code` 가 책 단위가 아닌 **상품 단위**임을 명시하고, 패키지·혼합 세트 규칙과 권장 필드 `isbn_list` 를 추가했습니다. ([상품 카탈로그](https://developers.solve.im/docs/partner-integration/product-catalog)) ## v1.0.0 — 2026-05-19 표준 API 를 처음 공개했습니다. ## 변경 정책 * **호환되지 않는 변경**은 라이브 제휴사에 미리 안내하고, 버전을 올려서 적용합니다. * **호환되는 변경**은 필드 추가처럼 하위 호환 범위 안에서만 진행합니다. * 정식 deprecation 정책은 실제로 변경이 생길 때 이 페이지에 추가합니다. # 자주 묻는 질문 URL: https://developers.solve.im/docs/partner-integration/faq > 제휴사 개발자와 담당자가 자주 묻는 질문을 모았습니다. 아니요. 쏠브가 제휴사 API 를 호출합니다. 제휴사가 쏠브를 호출하는 일은 [ⓑ 호스팅 로그인 페이지](https://developers.solve.im/docs/partner-integration/login-hosted-page) 방식의 콜백 1건뿐입니다. 아니요. 쏠브가 연동 직후, 회원의 책장 진입, "전체 동기화" 때 구매내역 API 를 호출해 최신 상태를 가져옵니다. ⓑ 를 쓰세요. 카카오 등 소셜로 가입한 회원은 ID/PW 가 없어서 ⓐ 로는 연동할 수 없습니다. 기준은 [개념과 용어](https://developers.solve.im/docs/partner-integration/overview)에 있습니다. 권장하지 않습니다. 아이디는 바뀔 수 있고, 바뀌면 회원이 연동과 책장의 책을 잃습니다. 내부 회원번호처럼 절대 바뀌지 않는 값을 쓰세요. 아니요. **`200` + 빈 `item_list`** 로 응답해 주세요. `404` 등 오류는 쏠브가 장애로 기록합니다. 자세한 표는 [구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api)에 있습니다. 구매내역 API 응답에서 그 상품을 빼면, 회원의 다음 동기화(책장 진입 등) 때 빠집니다. 실시간으로 빠지지는 않습니다. 단, 회원의 상품이 **전부** 환불돼 목록이 통째로 비면 쏠브는 장애와 구분할 수 없어 책을 빼지 않습니다. 이 경우는 쏠브 담당자와 협의해 주세요. 쏠브 파트너 어드민에 전자책을 직접 등록하면서 연동코드를 입력한다면 필요 없습니다. 쏠브가 전자책을 대신 등록하는 경우에만 보내주세요. [상품 연결](https://developers.solve.im/docs/partner-integration/product-catalog)을 참고하세요. 연동코드를 입력해 등록하면, 그전에 구매한 회원도 다음 동기화 때 책장에 책이 들어갑니다. 한 번에 모두 돌려주세요. 분할이 꼭 필요하면 미리 협의해 주세요. 회원 행동(책장 진입 등)에 따라 호출하므로 특정 시간에 몰릴 수 있습니다. rate limit 은 두지 않아도 되고, 둔다면 분당 600회 이상으로 잡아 주세요. 공개 샌드박스는 없습니다. 대신 제휴사 PC 에서 돌리는 점검 도구 두 가지가 있습니다. [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding)의 셀프 점검 도구(`solve-selfcheck.mjs`)는 구매내역 API 와 ⓐ 로그인 API 가 규약대로 응답하는지 확인하고, ⓑ 방식은 [콜백 흉내 도구](https://developers.solve.im/docs/partner-integration/login-hosted-page)(`solve-callback-mock.mjs`)로 계약 전에도 자기 로그인 페이지의 콜백을 시험할 수 있습니다. 먼저 확인한 뒤 테스트 계정을 보내주시면 쏠브가 실제 앱으로 검증합니다. 찾는 답이 없으면 쏠브 담당자에게 메일로 문의해 주세요. # 제휴사 연동 개요 URL: https://developers.solve.im/docs/partner-integration > 제휴사 사이트에서 구매한 책·강의를 쏠브 앱에서 그대로 이용하게 하는 연동입니다. 제휴사 사이트에서 책이나 강의를 구매한 회원이 쏠브 앱에서 **제휴사 계정으로 한 번 로그인**하면, 구매한 콘텐츠가 쏠브 책장에 자동으로 들어옵니다. 이 문서는 그 연동을 제휴사 개발자가 직접 구현할 수 있도록 정리한 가이드입니다. ## 3줄 요약 1. 제휴사는 **API 2개**(로그인, 구매내역)를 만듭니다. 쏠브가 이 API 를 호출합니다. 2. 전자책을 쏠브 파트너 어드민에 등록할 때 **연동코드**(제휴사 상품코드)를 입력합니다. 어드민을 쓰지 않으면 상품 목록 파일로 대신합니다. 3. 쏠브가 테스트 계정으로 앱 화면까지 확인한 뒤 출시합니다. ## 누가 무엇을 하나요? 쏠브가 **호출하는 쪽**, 제휴사가 **구현하는 쪽**입니다. 제휴사가 쏠브 API 를 호출하는 일은 ⓑ 방식의 콜백 1건뿐입니다. | 구분 | 제휴사 | 쏠브 | | ----- | ------------------------------------------ | ----------------------------- | | 로그인 | 로그인 API(ⓐ) 또는 로그인 페이지(ⓑ)를 구현 | 앱에서 로그인 화면을 띄우고 `user_key` 저장 | | 구매내역 | `user_key` 를 받아 현재 유효한 상품코드 목록을 반환 | 필요할 때 호출해 책장 권한을 부여·회수 | | 상품 연결 | 파트너 어드민에 전자책 등록 + 연동코드 입력 (또는 상품 목록 파일 전달) | 연동코드로 상품을 매칭 | | 테스트 | 테스트 계정과 URL 전달 | 실제 호출과 앱 화면으로 검증 | ## 한눈에 보기 회원의 로그인 방식에 따라 두 가지가 있습니다. 소셜 로그인(카카오·네이버·구글 등)을 함께 운영하면 **로그인 페이지 방식**입니다. ## 전체 흐름 (시퀀스) ## 어디서부터 볼까요? # ⓐ 자격증명 로그인 API URL: https://developers.solve.im/docs/partner-integration/login-credential-api > 쏠브가 회원의 ID/PW 를 보내면 확인하고 user_key 를 돌려주는, 제휴사가 구현하는 API 입니다. 회원이 쏠브 앱에 입력한 ID/PW 를 쏠브 서버가 그대로 보내면, 제휴사가 확인한 뒤 `user_key` 를 돌려주는 server-to-server API 입니다. 자체 ID/PW 로그인만 운영하는 제휴사에 권장합니다. 소셜 로그인을 함께 운영한다면 [ⓑ 호스팅 로그인 페이지](https://developers.solve.im/docs/partner-integration/login-hosted-page)를 쓰세요. ## 한눈에 보기 | 항목 | 내용 | | ------- | ------------------------------------------------- | | 호출하는 쪽 | 쏠브 서버 → 제휴사 서버 | | 메서드·URL | `POST`, URL 은 제휴사가 정합니다 | | 형식 | `Content-Type: application/json` 요청, JSON 응답 | | 인증 | 제휴사가 발급한 시크릿을 쏠브가 헤더에 담습니다. 헤더 이름과 방식은 제휴사가 정합니다. | | 타임아웃 | 호출당 10초 | | 재시도 | `5xx`·타임아웃이면 최대 2번 더 호출합니다. `4xx` 는 재시도하지 않습니다. | ## 요청 쏠브가 보내는 요청은 아래와 같습니다. 헤더 이름 `X-Solve-Secret` 은 예시입니다. ```bash title="쏠브가 보내는 요청 (curl 로 재현)" curl -X POST https://api.partner.example.com/solve/login \ -H 'Content-Type: application/json' \ -H 'X-Solve-Secret: <제휴사가 발급한 시크릿>' \ -d '{"id": "partner_user_01", "password": "p@ssw0rd"}' ``` | 필드 | 타입 | 필수 | 설명 | | ---------- | ------ | -- | --------------- | | `id` | string | 필수 | 회원이 입력한 로그인 아이디 | | `password` | string | 필수 | 회원이 입력한 비밀번호 | ## 응답 ### 성공 (200) ```json { "user_key": "100023" } ``` | 필드 | 타입 | 필수 | 설명 | | ---------- | ------ | -- | --------------------------------------------------------------- | | `user_key` | string | 필수 | 회원의 불변·유일 식별자, 최대 255자. 내부 회원번호를 권장합니다. 다른 필드를 더 담아도 쏠브는 무시합니다. | **응답 본문은 1MB(1,048,576바이트) 이하여야 합니다.** 쏠브는 1MB 를 넘는 응답을 받지 않습니다. 넘으면 그 로그인 호출은 실패로 처리하고 재시도하지 않습니다. 압축해서 보내는 경우는 푼 뒤의 크기로 셉니다. ### 실패 쏠브는 **상태 코드로 인증 실패와 시스템 오류를 구분**해 회원에게 다른 안내를 보여줍니다. 본문 형식은 자유입니다. | 상황 | 상태 코드 | 쏠브 앱에서 보이는 안내 | 재시도 | | ------------ | -------------- | ------------------------ | ----- | | 아이디·비밀번호 불일치 | `401` 등 `4xx` | "아이디 또는 비밀번호가 일치하지 않습니다" | 안 함 | | 시크릿 불일치 | `401` 또는 `403` | 위와 같음 | 안 함 | | 요청 형식 오류 | `400` 등 `4xx` | 위와 같음 | 안 함 | | 서버 오류, DB 장애 | `500` 등 `5xx` | 일반 오류 안내 | 최대 2번 | 쏠브는 `401` 과 `403` 을 구분하지 않고 같은 인증 실패로 처리합니다. 둘 다 재시도하지 않습니다. 장애 상황을 `4xx` 로 응답하면 회원은 비밀번호가 틀린 줄 알고 계속 다시 입력합니다. 예외가 나면 반드시 `5xx` 로 응답해 주세요. ## 구현 예시 시크릿 확인 → 입력값 확인 → 회원 인증 → `user_key` 응답 순서입니다. 회원 조회·비밀번호 검증 함수는 제휴사 시스템에 맞게 바꾸세요. Node.js Python Java PHP ```js import express from 'express'; import crypto from 'node:crypto'; const app = express(); app.use(express.json()); // 제휴사가 발급해 쏠브에 전달한 시크릿 const SOLVE_SECRET = Buffer.from(process.env.SOLVE_SECRET ?? ''); // 시크릿이 비어 있으면 빈 헤더 요청이 통과하므로 서버를 띄우지 않는다 if (SOLVE_SECRET.length === 0) throw new Error('SOLVE_SECRET is not set'); function isFromSolve(req) { const given = Buffer.from(req.get('X-Solve-Secret') ?? ''); return given.length === SOLVE_SECRET.length && crypto.timingSafeEqual(given, SOLVE_SECRET); } app.post('/solve/login', async (req, res) => { if (!isFromSolve(req)) return res.status(403).json({ error: 'forbidden' }); const { id, password } = req.body ?? {}; if (typeof id !== 'string' || typeof password !== 'string') { return res.status(400).json({ error: 'invalid_request' }); } try { const member = await findMemberByLoginId(id); if (!member || !(await verifyPassword(password, member.passwordHash))) { return res.status(401).json({ error: 'invalid_credentials' }); } // 로그인 아이디가 아니라 절대 바뀌지 않는 내부 회원번호 return res.json({ user_key: String(member.memberNo) }); } catch (err) { console.error('solve login failed', err); return res.status(500).json({ error: 'internal_error' }); } }); ``` ```python import hmac import logging import os from fastapi import FastAPI, Header from fastapi.responses import JSONResponse from pydantic import BaseModel app = FastAPI() logger = logging.getLogger(__name__) SOLVE_SECRET = os.environ["SOLVE_SECRET"] # 제휴사가 발급해 쏠브에 전달한 시크릿 class LoginRequest(BaseModel): id: str password: str @app.post("/solve/login") def login(body: LoginRequest, x_solve_secret: str = Header(default="")): if not hmac.compare_digest(x_solve_secret, SOLVE_SECRET): return JSONResponse({"error": "forbidden"}, status_code=403) try: member = find_member_by_login_id(body.id) if member is None or not verify_password(body.password, member.password_hash): return JSONResponse({"error": "invalid_credentials"}, status_code=401) # 로그인 아이디가 아니라 절대 바뀌지 않는 내부 회원번호 return {"user_key": str(member.member_no)} except Exception: logger.exception("solve login failed") return JSONResponse({"error": "internal_error"}, status_code=500) ``` ```java @RestController @RequestMapping("/solve") public class SolveLoginController { private static final Logger log = LoggerFactory.getLogger(SolveLoginController.class); @Value("${solve.secret}") // 제휴사가 발급해 쏠브에 전달한 시크릿 private String solveSecret; private final MemberService memberService; public SolveLoginController(MemberService memberService) { this.memberService = memberService; } public record LoginRequest(String id, String password) {} @PostMapping("/login") public ResponseEntity> login( @RequestHeader(value = "X-Solve-Secret", defaultValue = "") String secret, @RequestBody LoginRequest req) { if (!MessageDigest.isEqual( secret.getBytes(StandardCharsets.UTF_8), solveSecret.getBytes(StandardCharsets.UTF_8))) { return ResponseEntity.status(403).body(Map.of("error", "forbidden")); } if (req.id() == null || req.password() == null) { return ResponseEntity.badRequest().body(Map.of("error", "invalid_request")); } try { Optional member = memberService.authenticate(req.id(), req.password()); if (member.isEmpty()) { return ResponseEntity.status(401).body(Map.of("error", "invalid_credentials")); } // 로그인 아이디가 아니라 절대 바뀌지 않는 내부 회원번호 return ResponseEntity.ok(Map.of("user_key", String.valueOf(member.get().getMemberNo()))); } catch (Exception e) { log.error("solve login failed", e); return ResponseEntity.status(500).body(Map.of("error", "internal_error")); } } } ``` ```php 'forbidden']); exit; } $body = json_decode(file_get_contents('php://input'), true); $id = $body['id'] ?? null; $password = $body['password'] ?? null; if (!is_string($id) || !is_string($password)) { http_response_code(400); echo json_encode(['error' => 'invalid_request']); exit; } try { $member = find_member_by_login_id($id); if (!$member || !password_verify($password, $member['password_hash'])) { http_response_code(401); echo json_encode(['error' => 'invalid_credentials']); exit; } // 로그인 아이디가 아니라 절대 바뀌지 않는 내부 회원번호 echo json_encode(['user_key' => (string) $member['member_no']]); } catch (Throwable $e) { error_log('solve login failed: ' . $e->getMessage()); http_response_code(500); echo json_encode(['error' => 'internal_error']); } ``` ## 흐름 ## 구현 체크리스트 * [ ] 로그인에 성공하면 내부 회원번호처럼 **바뀌지 않는 값**을 `user_key` 로 돌려준다 * [ ] 비밀번호 불일치는 `4xx`, 서버 오류는 `5xx` 로 응답한다 * [ ] 응답 본문을 1MB(1,048,576바이트) 이하로 만든다 * [ ] 시크릿을 상수 시간 비교(`timingSafeEqual`, `hash_equals` 등)로 확인한다 * [ ] ID/PW 를 로그에 남기지 않는다 * [ ] 같은 요청을 여러 번 받아도 데이터가 바뀌지 않는다 (쏠브가 재시도할 수 있음) 체크리스트 항목 중 응답으로 확인할 수 있는 것(맞는·틀린 비밀번호, 틀린 시크릿, `user_key` 형식, 응답 시간)은 [전체 점검 도구](https://developers.solve.im/docs/partner-integration/onboarding)로 한 번에 점검할 수 있습니다. ## 관련 문서 * [구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api) · [보안 요구사항](https://developers.solve.im/docs/partner-integration/security) · [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding) # ⓑ 호스팅 로그인 페이지 URL: https://developers.solve.im/docs/partner-integration/login-hosted-page > 제휴사 로그인 페이지에서 인증한 뒤 쏠브 콜백을 호출하는 방식입니다. 소셜 로그인을 운영하는 제휴사용입니다. 쏠브 앱이 제휴사의 로그인 페이지를 열면, 회원은 **평소처럼 제휴사 사이트에서 로그인**합니다. 로그인이 끝나면 제휴사 서버가 쏠브 콜백 API 를 호출하고, 받은 `redirect_url` 로 회원을 보내면 연동이 완료됩니다. 카카오·네이버·구글·Apple 같은 소셜 로그인을 운영하거나, 자체 ID/PW 와 소셜 로그인을 함께 운영하는 제휴사는 이 방식을 사용합니다. 계약 후 쏠브가 **콜백 API URL**, 제휴사의 **`platform_type`**(숫자), **콜백 인증 시크릿**을 전달합니다. 제휴사는 **쏠브 전용 로그인 페이지 URL** 을 쏠브에 알려주세요. ## 흐름 ## 1단계. 쏠브 전용 로그인 페이지 준비 * 쏠브 앱은 등록된 URL 을 **그대로** 엽니다. 쿼리 파라미터를 붙이지 않습니다. * 일반 로그인과 구분할 수 있도록 **쏠브 전용 URL**(예: `https://www.partner.example.com/solve/login`)을 따로 두세요. 이 URL 로 들어온 로그인만 쏠브 콜백으로 이어집니다. * 화면 구성과 인증 방식은 자유입니다. 기존 로그인 화면을 그대로 써도 됩니다. * 소셜 로그인처럼 외부 사이트를 다녀오는 경우, 세션에 "쏠브 연동 중" 표시를 남겨 두었다가 로그인이 끝나는 시점에 2단계를 진행하세요. ## 2단계. 로그인 성공 후 쏠브 콜백 호출 로그인에 **성공했을 때만** 제휴사 **서버**에서 호출합니다. 콜백 인증 시크릿이 들어가므로 브라우저에서 호출하면 안 됩니다. ```bash title="콜백 요청 (curl 로 재현)" curl -X POST '<쏠브가 전달한 콜백 API URL>' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <쏠브가 발급한 콜백 인증 시크릿>' \ -d '{"id": "100023", "platform_type": <쏠브가 부여한 platform_type>}' ``` | 헤더 | 값 | 설명 | | --------------- | -------------- | -------------------------------------------------------------- | | `Authorization` | `Bearer <시크릿>` | 쏠브가 제휴사별로 발급한 콜백 인증 시크릿. 서버 설정(환경변수 등)에만 두고 로그·브라우저에 노출하지 마세요. | | 필드 | 타입 | 필수 | 설명 | | ------------------------ | ------ | -- | ------------------------------------ | | `id` | string | 필수 | 회원의 불변·유일 식별자(= `user_key`), 최대 255자 | | `platform_type` | number | 필수 | 쏠브가 부여한 제휴사 식별자 | | `solve_sso_verify_token` | string | 선택 | 과거 버전 호환용입니다. 보내도 무시하며 생략해도 됩니다. | 카카오·네이버 등으로 로그인했더라도 **제휴사 내부 회원의 불변 식별자**(회원번호 등)를 보내야 합니다. 값이 바뀌면 회원은 연동과 책장의 책을 잃고, 탈퇴한 회원의 값을 다른 회원에게 다시 주면 안 됩니다. ### 응답 | 상태 코드 | 본문 | 의미와 처리 | | ----- | ------------------------------------------------------ | ------------------------------------------------------------------------------ | | `200` | `{ "redirect_url": "https://..." }` | 3단계로 진행합니다. | | `400` | `{ "error": "BadRequest", "message": "..." }` | `platform_type` 이 틀렸거나 비활성 상태입니다. 쏠브에 문의해 주세요. | | `401` | `{ "error": "Unauthorized", "message": "..." }` | 인증 시크릿이 없거나 틀렸습니다. 재시도하지 말고 설정을 확인하세요. | | `429` | | 쏠브 콜백은 분당 600회까지 받습니다. 호출 한도를 넘었습니다. 응답의 `Retry-After` 헤더(초)만큼 기다린 뒤 다시 시도하세요. | | `500` | `{ "error": "InternalServerError", "message": "..." }` | 쏠브 일시 오류입니다. 회원에게 잠시 후 다시 시도하도록 안내하세요. | ## 3단계. 같은 웹뷰에서 redirect\_url 로 이동 받은 `redirect_url` 로 회원의 브라우저를 **그대로** 이동시킵니다(HTTP 302 또는 `window.location.href`). URL 에는 쏠브가 위변조 방지용 서명 파라미터를 함께 담아 보내므로, 파라미터를 빼거나 순서를 바꾸는 등 **어떤 수정도 하지 마세요.** 쏠브는 연동을 시작한 웹뷰에 1회용 토큰을 발급해 두고, `redirect_url` 로 돌아왔을 때 확인합니다. 로그인부터 이동까지 **쏠브 앱이 연 같은 웹뷰 안에서** 이어져야 합니다. `target="_blank"`, 외부 브라우저 열기, 팝업 로그인 뒤 부모 창 이동 등을 쓰지 마세요. 재생 공격·CSRF 방지는 이 1회용 토큰으로 쏠브가 처리하므로, 제휴사가 `state`·nonce 를 따로 구현할 필요는 없습니다. ## 구현 예시 제휴사 로그인이 성공한 직후 호출하는 함수입니다. 일반 로그인 핸들러와 소셜 로그인 콜백 핸들러 양쪽에서, "쏠브 연동 중"인 세션일 때만 호출하세요. Node.js Python Java PHP ```js // 제휴사 로그인 성공 직후, 쏠브 연동 중인 세션에서만 호출 async function completeSolveLink(req, res, member) { const response = await fetch(process.env.SOLVE_CALLBACK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.SOLVE_CALLBACK_SECRET}`, // 쏠브가 발급한 콜백 인증 시크릿 }, body: JSON.stringify({ id: String(member.memberNo), // 소셜 ID 가 아닌 내부 회원번호 platform_type: Number(process.env.SOLVE_PLATFORM_TYPE), }), signal: AbortSignal.timeout(10_000), }); if (!response.ok) { console.error('solve callback failed', response.status); return res.status(502).send('쏠브 연동 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'); } const { redirect_url } = await response.json(); delete req.session.solveLinking; return res.redirect(302, redirect_url); // 같은 웹뷰에서 이동 } ``` ```python import logging import os import requests from fastapi import Request from fastapi.responses import PlainTextResponse, RedirectResponse logger = logging.getLogger(__name__) def complete_solve_link(request: Request, member): """제휴사 로그인 성공 직후, 쏠브 연동 중인 세션에서만 호출""" try: resp = requests.post( os.environ["SOLVE_CALLBACK_URL"], headers={"Authorization": f"Bearer {os.environ['SOLVE_CALLBACK_SECRET']}"}, # 쏠브 발급 시크릿 json={ "id": str(member.member_no), # 소셜 ID 가 아닌 내부 회원번호 "platform_type": int(os.environ["SOLVE_PLATFORM_TYPE"]), }, timeout=10, ) resp.raise_for_status() except requests.RequestException: logger.exception("solve callback failed") return PlainTextResponse("쏠브 연동 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.", status_code=502) request.session.pop("solve_linking", None) return RedirectResponse(resp.json()["redirect_url"], status_code=302) # 같은 웹뷰에서 이동 ``` ```java // 제휴사 로그인 성공 직후, 쏠브 연동 중인 세션에서만 호출 // restClient(RestClient), solveCallbackUrl·solvePlatformType·solveCallbackSecret(설정값), log(Logger)는 클래스 필드 public String completeSolveLink(HttpSession session, Member member) { Map body = Map.of( "id", String.valueOf(member.getMemberNo()), // 소셜 ID 가 아닌 내부 회원번호 "platform_type", solvePlatformType ); try { Map result = restClient.post() .uri(solveCallbackUrl) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + solveCallbackSecret) // 쏠브 발급 시크릿 .body(body) .retrieve() .body(Map.class); session.removeAttribute("solveLinking"); return "redirect:" + result.get("redirect_url"); // 같은 웹뷰에서 이동 } catch (RestClientException e) { log.error("solve callback failed", e); return "solve/link-error"; } } ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . getenv('SOLVE_CALLBACK_SECRET'), // 쏠브 발급 시크릿 ], CURLOPT_POSTFIELDS => json_encode([ 'id' => (string) $member['member_no'], // 소셜 ID 가 아닌 내부 회원번호 'platform_type' => (int) getenv('SOLVE_PLATFORM_TYPE'), ]), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($raw === false || $status !== 200) { error_log("solve callback failed: {$status}"); http_response_code(502); echo '쏠브 연동 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'; exit; } unset($_SESSION['solve_linking']); header('Location: ' . json_decode($raw, true)['redirect_url'], true, 302); // 같은 웹뷰에서 이동 exit; } ``` ## 콜백을 미리 시험하기 콜백 주소와 시크릿은 계약 뒤에 받지만, 그 전에도 제휴사 로그인 페이지가 콜백을 규약대로 보내는지 확인할 수 있습니다. [solve-callback-mock.mjs 내려받기](/tools/solve-callback-mock.mjs)로 자기 PC 에 "쏠브 콜백 API 흉내"를 띄우세요. 파일 하나이며 Node.js 18 이상만 있으면 설치할 것이 없고, 받기만 할 뿐 밖으로는 아무것도 보내지 않습니다. 쏠브 서버와는 아무 관계가 없습니다. 시험용 시크릿과 `platform_type` 을 환경변수로 넘겨 실행합니다. 시크릿은 직접 정하되 쏠브가 받아 주는 값과 같은 조건이어야 합니다 — 32자 이상 4096자 이하, 영문·숫자·기호(공백 포함)만, 앞뒤 공백 없이. 쏠브는 32자 미만의 콜백 시크릿을 쓸 수 없는 값으로 보고 모든 콜백을 `401` 로 거절하므로, 짧은 값으로는 도구가 시작하지 않습니다. `platform_type` 은 쏠브가 부여하는 양의 정수입니다(`0` 이나 소수는 안 됩니다). ```bash title="콜백 흉내 도구 실행 (macOS·Linux)" SOLVE_MOCK_CALLBACK_SECRET=<시험용 값> SOLVE_MOCK_PLATFORM_TYPE=<숫자> node solve-callback-mock.mjs ``` ```powershell title="콜백 흉내 도구 실행 (Windows PowerShell)" $env:SOLVE_MOCK_CALLBACK_SECRET = "<시험용 값>"; $env:SOLVE_MOCK_PLATFORM_TYPE = "<숫자>"; node solve-callback-mock.mjs ``` 도구가 시작하면 `http://127.0.0.1:4310/solve-callback` 처럼 콜백 주소를 알려 줍니다. 제휴사 서버의 "쏠브 콜백 URL" 을 그 주소로, 콜백 시크릿을 위에서 정한 시험용 값으로, `platform_type` 을 같은 숫자로 바꾼 뒤 자기 로그인 페이지에서 로그인해 보세요. 쏠브가 계약 뒤에 전달하는 진짜 콜백 시크릿은 이 도구에 넣지 마세요. | 확인하는 것 | 결과 | | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | | 정상 요청 | `200` 과 `redirect_url` | | 시크릿이 없거나 틀림 | `401` | | `id` 가 문자열이 아니거나 비었거나 256자 이상, `platform_type` 이 양의 정수가 아니거나 없거나 다름, `solve_sso_verify_token` 이 있는데 문자열이 아님(`null` 포함), `Content-Type` 이 JSON 이 아님 | `400` | | `platform_type` 이 숫자 모양의 문자열 | `200`, 단 콘솔에 "숫자로 보내세요" 주의 | | `POST` 가 아닌 요청 | `405` | 본문이 틀리면 시크릿이 틀려도 `400` 입니다 — 쏠브도 인증보다 본문을 먼저 보기 때문이며, 콘솔 줄에는 시크릿 문제도 함께 나옵니다. 요청마다 콘솔에 점검 결과가 한 줄씩 나옵니다. 시크릿 값은 찍지 않고 "일치"·"불일치"만 알려 줍니다. 받은 요청의 본문·헤더·주소 원문도 화면에 싣지 않고, `id` 는 길이만, 필드는 이름이 아닌 통과 여부만 알려 줍니다. `--next 429` 또는 `--next 500` 을 붙이면 다음 정상 요청 한 번에 그 응답(`429` 는 `Retry-After: 1`)을 돌려주므로 제휴사의 오류 처리도 시험할 수 있습니다. `--port`·`--host` 로 주소를 바꿀 수 있고, `--host` 를 `127.0.0.1` 밖으로 바꾸면 같은 네트워크의 다른 기기에서도 접근됩니다. 도구가 돌려준 `redirect_url` 은 `/done` 주소입니다. 제휴사 서버가 이 주소로 회원의 브라우저를 수정 없이 보내면 "연동 흉내가 끝났습니다" 화면에 `PASS` 가 나오고, 쿼리를 하나라도 바꾸면 `FAIL` 과 달라진 값의 개수(빠짐·값이 바뀜·추가됨)가 나옵니다. 이름과 값은 화면에 싣지 않습니다. 쏠브가 만든 연동 주소의 서명은 10분 뒤 만료되므로, 도구도 `redirect_url` 을 받은 지 10분이 지나서 열면 `FAIL` 로 알립니다. 주소의 `sig` 같은 값은 서명처럼 보이게 만든 임의 값이며 진짜 서명이 아닙니다. ## 구현 체크리스트 * [ ] 쏠브 전용 로그인 URL 을 쏠브에 전달했다 (쿼리 파라미터 없이 동작) * [ ] 로그인에 **성공했을 때만**, 제휴사 **서버**에서 `Authorization: Bearer <시크릿>` 을 담아 콜백을 호출한다 * [ ] 콜백 인증 시크릿이 브라우저·로그에 노출되지 않는다 * [ ] `id` 로 소셜 계정 ID·이메일이 아닌 내부 회원의 불변 식별자를 보낸다 * [ ] `redirect_url` 을 수정 없이, 같은 웹뷰에서 연다 * [ ] 소셜 로그인으로 외부를 다녀와도 "쏠브 연동 중" 상태가 유지된다 ## 관련 문서 * [구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api) · [보안 요구사항](https://developers.solve.im/docs/partner-integration/security) · [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding) # 테스트와 출시 URL: https://developers.solve.im/docs/partner-integration/onboarding > 셀프 점검, 쏠브에 보낼 테스트 자료, 쏠브 검증 항목, 출시 체크리스트를 정리했습니다. 구현이 끝나면 **셀프 점검 → 테스트 자료 전달 → 쏠브 검증 → 출시** 순서로 진행합니다. 공개 샌드박스는 없으며, 제휴사 테스트 환경(또는 운영 환경의 테스트 계정)으로 검증합니다. ## 1. 셀프 점검 쏠브에 자료를 보내기 전에, 제휴사 서버가 규약대로 응답하는지 아래 도구로 확인해 보세요. 두 도구 모두 제휴사 PC 에서 돌리며, 쏠브 서버로는 아무것도 보내지 않습니다. ⓑ 호스팅 로그인 페이지 방식이라면 콜백 주소와 시크릿을 계약 뒤에야 받습니다. 그 전에는 [콜백 흉내 도구](https://developers.solve.im/docs/partner-integration/login-hosted-page)로 자기 로그인 페이지가 콜백을 규약대로 보내는지 미리 확인해 보세요. ### 전체 점검 (Node) Node.js 18 이상이 있으면 구매내역 API 와 ⓐ 로그인 API 를 한 번에 점검할 수 있습니다. 응답 형식, 선택 필드(v1.1), 오류 응답, 응답 시간(10초)까지 확인하고 점검마다 `PASS`·`WARN`·`FAIL`·`SKIP` 으로 알려 줍니다. [solve-selfcheck.mjs 내려받기](/tools/solve-selfcheck.mjs) — 파일 하나이며 설치할 것이 없습니다. 브라우저에 코드가 그대로 열리면 "다른 이름으로 저장"으로 받아 주세요. 먼저 설정 파일 틀을 만듭니다. 같은 이름의 파일이 이미 있으면 덮어쓰지 않습니다. ```bash title="설정 파일 틀 만들기" node solve-selfcheck.mjs --init ``` 만들어진 `solve-selfcheck.config.json` 에 제휴사 값을 적습니다. ```json title="solve-selfcheck.config.json" { "purchaseUrl": "https://api.partner.example.com/solve/purchases", "authHeaderName": "X-Solve-Secret", "secret": "여기에-점검용-시크릿", "userKey": "여기에-구매-내역이-있는-회원의-user_key", "loginUrl": "", "loginId": "", "loginPassword": "" } ``` | 설정 | 설명 | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `purchaseUrl` | 구매내역 API URL. 2048자 이하이고 공백이나 `아이디:비밀번호@` 를 넣을 수 없습니다 | | `authHeaderName`, `secret` | 쏠브가 보낼 인증 헤더 이름과 값. 이름은 영문·숫자·기호 64자 이하이며 `Host`·`Content-Type`·`Cookie`·`Proxy-*`·`X-Forwarded-*` 같은 이름은 쓸 수 없습니다. 값은 16자 이상 4096자 이하이고 앞뒤에 공백이나 탭이 없어야 합니다 | | `userKey` | 구매 내역이 있는 테스트 회원의 `user_key`. 앞뒤 공백까지 그대로 씁니다 | | `loginUrl`, `loginId`, `loginPassword` | ⓐ 방식만 적습니다. 로그인 API URL 과 테스트 계정입니다. 비워 두면 로그인 점검은 건너뜁니다 | ```bash title="점검 실행" node solve-selfcheck.mjs solve-selfcheck.config.json ``` ```text title="모두 통과하면" 쏠브 셀프 점검 v2 (도구 1.0.0) 구매내역 API: https://api.partner.example.com (경로와 쿼리는 화면에 싣지 않습니다) 로그인 API: https://api.partner.example.com 호출마다 10.00초 제한을 두고 걸린 시간을 보여 드립니다. PASS [1] 구매내역 · 구매 회원 조회 (HTTP 200, 0.21초) item_list 2개, 모든 항목에 item_code 가 있습니다 PASS [2] 구매내역 · 없는 회원 조회 (HTTP 200, 0.09초) 200 과 빈 item_list 로 응답했습니다 PASS [3] 구매내역 · 틀린 시크릿 (HTTP 403, 0.05초) 403 응답으로 거절했습니다 PASS [4] 구매내역 · 인증 헤더 없음 (HTTP 403, 0.04초) 403 응답으로 거절했습니다 PASS [5] 구매내역 · user_key 없는 요청 (HTTP 400, 0.04초) 400 응답으로 거절했습니다 SKIP [6] 구매내역 · 선택 필드 형식 선택 필드 없음 — 없어도 됩니다 PASS [8] 로그인 · 맞는 아이디·비밀번호 (HTTP 200, 0.12초) user_key 를 받았습니다 (6자) PASS [9] 로그인 · 틀린 비밀번호 (HTTP 401, 0.10초) 401 응답으로 거절했습니다 PASS [10] 로그인 · 틀린 시크릿 (HTTP 403, 0.04초) 403 응답으로 거절했습니다 PASS [11] 로그인 + 구매내역 · 로그인과 구매내역의 user_key 로그인이 돌려준 user_key 가 설정의 userKey 와 같습니다 PASS [7] 공통 · 응답 시간 모든 호출이 10.00초 안에 끝났습니다 (가장 느린 호출 0.21초) PASS [12] 공통 · 주소가 https 인지 https 입니다 (같은 PC 의 localhost 는 예외) 요약: 통과 11 · 주의 0 · 실패 0 · 건너뜀 1 실패는 없습니다. 참고: 이 도구의 통과는 운영 등록 가능의 증거가 아닙니다. http 주소는 실제 등록에서 거부됩니다. ``` `FAIL` 이나 `WARN` 이 나오면 바로 아래에 고치는 방법과 응답 요약이 함께 나옵니다. 응답 요약은 본문의 바이트 수, JSON 의 종류, 규약에 있는 필드의 타입과 길이, 그 밖의 필드 개수만 보여 줍니다. 이 도구는 응답의 본문·헤더 원문과 시크릿·비밀번호를 화면에 싣지 않으며, 주소도 경로와 쿼리 없이 `https://호스트` 까지만 보여 줍니다. ```text title="고칠 것이 있으면" FAIL [2] 구매내역 · 없는 회원 조회 (HTTP 404, 0.04초) 200 이 아니라 404 응답을 보냈습니다 고치는 방법: 없는 회원은 404 나 오류가 아니라 200 과 { "item_list": [] } 로 응답해 주세요. 응답 요약: 본문 21바이트 · JSON 객체 · error: 문자열 9자 ``` | 결과 | 뜻 | | ------ | --------------------------------------------- | | `PASS` | 규약대로입니다 | | `WARN` | 쏠브는 동작하지만 권장과 다릅니다. 고치는 방법을 확인해 주세요 | | `FAIL` | 규약과 다릅니다. 고친 뒤 다시 실행해 주세요 | | `SKIP` | 해당하지 않아 건너뛰었습니다 (선택 필드가 없거나 로그인 주소를 적지 않은 경우) | 이 도구는 쏠브가 응답을 읽는 방식과 같은 기준으로 판정합니다. 쏠브가 응답 전체를 쓰지 못하는 경우는 `FAIL`, 응답은 받되 그 필드만 버리는 경우는 `WARN` 입니다. 위 설정 값이 쏠브가 받아 주는 조건을 어기면 점검하지 않고 종료 코드 `2` 로 끝납니다. | 점검 | `FAIL` — 쏠브가 응답을 쓰지 못합니다 | `WARN` — 쏠브가 그 필드만 버립니다 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1·2번 구매내역 응답 | `200` 이 아님(`3xx` 포함) · JSON 객체가 아님 · `item_list` 가 배열이 아님 · 항목이 객체가 아님 · `item_code` 가 문자열이 아니거나, 앞뒤 공백을 뗀 뒤 비어 있거나 255자를 넘음. 항목 하나만 틀려도 목록 전체를 버립니다 | — | | 6번 선택 필드 | — | `price` 가 0 이상의 안전한 정수(9007199254740991 이하)나 1~~12자리 숫자 문자열이 아님 · `paid_at` 이 존재하는 날짜의 ISO 8601 이 아니거나 오프셋의 시가 23, 분이 59 를 넘음 · `valid_from`·`valid_until` 이 앞뒤 공백을 뗀 뒤 1~~32자 문자열이 아님 · `item_name` 이 문자열이 아님. `null` 은 보내지 않은 것과 같습니다 | | 8번 로그인 응답 | `200` 이 아님(`3xx` 포함) · JSON 객체가 아님 · `user_key` 가 문자열이 아니거나 공백뿐이거나 255자를 넘음 | — | 응답 본문은 1MB(1,048,576바이트) 까지만 읽습니다. 정확히 1MB 는 통과하고, 1바이트라도 넘으면 그 호출은 "응답이 1MB 를 넘습니다. 쏠브는 1MB 를 넘는 응답을 받지 않습니다." 와 함께 `FAIL` 입니다. 쏠브도 1MB 를 넘는 응답을 받지 않기 때문입니다. 잘린 본문으로는 어떤 점검도 `PASS` 로 판정하지 않습니다. 구매내역·로그인 응답은 필요한 필드만 담아 1MB 이하로 줄여 주세요. 종료 코드는 `FAIL` 이 없으면 `0`, 하나라도 있으면 `1`, 설정이나 실행 환경에 문제가 있으면 `2` 입니다. 이 도구는 설정 파일에 적은 주소만 호출하고 점검 하나에 호출 하나만 보내며, 리다이렉트를 따라가지 않습니다. 쏠브도 다른 주소로 넘기는 응답을 따라가지 않으므로 `3xx` 응답은 `FAIL` 이고, 이때는 설정에 최종 주소를 적어 주세요. 쏠브 서버 주소는 점검 대상이 아니라서 호출하지 않고 종료 코드 `2` 로 끝납니다. 로그인 점검은 틀린 비밀번호로 한 번 로그인을 시도하므로, 계정 잠금 정책이 있다면 테스트 계정으로 하세요. 이 도구의 통과는 운영 등록 가능의 증거가 아닙니다. 12번이 `WARN` 인 `http` 주소는 종료 코드 `0` 이어도 쏠브에 실제로 등록할 때 거부됩니다. ### 빠른 확인 (bash) `curl` 과 `bash` 만 있으면 구매내역 API 3건을 바로 확인할 수 있습니다. 맨 위 4개 값만 바꾸면 됩니다. ```bash title="solve-selfcheck.sh" #!/usr/bin/env bash # 쏠브 연동 셀프 점검 — 구매내역 API URL="https://api.partner.example.com/solve/purchases" # 구매내역 API URL SECRET_HEADER="X-Solve-Secret: 여기에-시크릿" # 쏠브가 보낼 인증 헤더 USER_KEY="100023" # 구매 내역이 있는 테스트 회원 UNKNOWN_KEY="solve-selfcheck-no-such-user" # 존재하지 않는 회원 BODY_FILE=$(mktemp) call() { # $1=인증 헤더 $2=요청 본문 → "상태코드 소요초" 출력 curl -s -o "$BODY_FILE" -w '%{http_code} %{time_total}' --max-time 10 \ -X POST "$URL" -H 'Content-Type: application/json' -H "$1" -d "$2" } report() { # $1=점검명 $2=통과 여부(0/1) $3=상세 if [ "$2" = 1 ]; then echo "PASS $1 ($3)"; else echo "FAIL $1 ($3)"; fi echo " 응답: $(head -c 300 "$BODY_FILE")" } read -r code secs <<<"$(call "$SECRET_HEADER" "{\"user_key\":\"$USER_KEY\"}")" ok=0; [ "$code" = 200 ] && tr -d ' \n\r\t' < "$BODY_FILE" | grep -q '"item_code":"' && ok=1 report "구매 회원 → 200 + item_code 포함" $ok "HTTP $code, ${secs}s" read -r code secs <<<"$(call "$SECRET_HEADER" "{\"user_key\":\"$UNKNOWN_KEY\"}")" ok=0; [ "$code" = 200 ] && tr -d ' \n\r\t' < "$BODY_FILE" | grep -q '"item_list":\[\]' && ok=1 report "없는 회원 → 200 + 빈 item_list" $ok "HTTP $code, ${secs}s" read -r code secs <<<"$(call "X-Solve-Secret: wrong-secret" "{\"user_key\":\"$USER_KEY\"}")" ok=0; [[ "$code" =~ ^4 ]] && ok=1 report "틀린 시크릿 → 4xx" $ok "HTTP $code, ${secs}s" rm -f "$BODY_FILE" ``` ```text title="모두 통과하면" PASS 구매 회원 → 200 + item_code 포함 (HTTP 200, 0.21s) PASS 없는 회원 → 200 + 빈 item_list (HTTP 200, 0.09s) PASS 틀린 시크릿 → 4xx (HTTP 403, 0.05s) ``` ⓐ 방식이라면 [로그인 API 의 curl 예시](https://developers.solve.im/docs/partner-integration/login-credential-api)로 맞는 비밀번호(`200` + `user_key`)와 틀린 비밀번호(`4xx`)도 확인해 보세요. ## 2. 쏠브에 보낼 테스트 자료 아래 자료를 쏠브 담당자 이메일로 보내주세요. 담당자 주소는 계약 시 안내해 드립니다. | 자료 | ⓐ 자격증명 API | ⓑ 호스팅 로그인 페이지 | | ------ | --------------------------- | --------------------------------- | | 로그인 | 로그인 API URL | 쏠브 전용 로그인 페이지 URL | | 구매내역 | 구매내역 API URL | 구매내역 API URL | | 인증 | 시크릿과 헤더 이름 | 시크릿과 헤더 이름 | | 테스트 계정 | ID/PW (구매 내역이 있는 계정) | 로그인 가능한 테스트 회원 + 그 회원의 `user_key` | | 비교용 | 그 계정이 보유한 `item_code` 와 상품명 | 같음 | 시크릿·소셜 계정처럼 민감한 정보는 메일 본문 대신 별도 보안 채널로 전달 방법을 협의합니다. ## 3. 쏠브가 확인하는 것 | 구분 | 확인 내용 | | --- | ------------------------------------------------- | | API | 응답 형식, `user_key` 가 매번 같은지, 환불 항목이 빠지는지, 오류 코드 구분 | | 연동 | 쏠브 앱에서 로그인 → 연동 완료까지 (ⓑ 는 같은 웹뷰 안에서 끝나는지) | | 책장 | 테스트 계정의 `item_code` 에 맞는 책이 책장에 보이는지 | | 회수 | 테스트 계정에서 상품을 뺐을 때 다음 동기화에 책장에서 빠지는지 | 제휴사에서도 같은 계정으로 응답을 점검해 주세요. ## 4. 출시 전 체크리스트 * [ ] 로그인(ⓐ 또는 ⓑ)이 동작하고, `user_key` 가 불변·유일하다 * [ ] 구매내역 API 가 환불·만료 항목을 빼고, 없는 회원에게 빈 배열(200)을 준다 * [ ] 장애·부분 조회 실패 때는 `5xx` 로 응답한다 * [ ] 판매 중인 전자책마다 연동코드가 입력됐다 (카탈로그 파일 방식이면 전체 목록을 전달했다) * [ ] 모든 통신이 HTTPS 이고, 서버 간 호출에 시크릿 인증이 적용됐다 * [ ] 테스트 계정으로 쏠브 앱 책장에 책이 보인다 * [ ] 롤백 방법과 연락 창구를 양쪽이 합의했다 ## 출시 후 * **회원 안내**: 연동은 회원이 쏠브 앱에서 직접 시작하므로, 제휴사 사이트에서 쏠브 앱을 안내하면 이용률이 크게 올라갑니다. [구매 후 사용자 안내](https://developers.solve.im/docs/partner-integration/user-discovery)를 참고하세요. * **시크릿 회전·URL 변경**: 미리 알려 주세요. 새 값(주소)과 옛 값(주소)을 **함께 받아 주시는 동안** 쏠브가 새 값으로 바꿉니다(반영까지 최대 5분). 바뀐 것을 확인하신 뒤 옛 값을 폐기하시면 됩니다. ([보안 요구사항](https://developers.solve.im/docs/partner-integration/security)) * **롤백**: 연동을 끄면 신규 연동과 동기화가 멈춥니다. 이미 들어간 책은 남습니다. 회수가 필요하면 쏠브 담당자와 범위·시점을 협의해 주세요. # 개념과 용어 URL: https://developers.solve.im/docs/partner-integration/overview > 연동의 2단계 구조, 꼭 알아야 할 용어, 로그인 방식 고르는 법을 설명합니다. 구현을 시작하기 전에 알아야 할 개념을 정리했습니다. 이 페이지만 읽어도 전체 구조를 이해할 수 있습니다. ## 연동은 두 단계입니다 1. **연동(Linking)**: 회원이 쏠브 앱에서 제휴사 계정으로 로그인하면, 쏠브가 그 회원의 `user_key` 를 저장합니다. 회원당 한 번만 일어납니다. 2. **동기화(Sync)**: 쏠브가 `user_key` 로 구매내역 API 를 호출해, 받은 상품코드에 맞는 책을 책장에 넣거나 뺍니다. 동기화는 쏠브가 필요할 때 제휴사 API 를 호출하는 방식(pull)입니다. 제휴사가 쏠브에 구매 소식을 따로 알려줄 필요는 없습니다. | 동기화 시점 | 설명 | | ------ | ----------------------------- | | 연동 직후 | 로그인이 끝나면 1회 자동으로 실행됩니다. | | 책장 진입 | 회원이 쏠브 앱의 책장에 들어갈 때 실행됩니다. | | 전체 동기화 | 회원이 앱에서 "전체 동기화"를 누를 때 실행됩니다. | ## 용어 | 용어 | 뜻 | | --------------- | -------------------------------------------------------------------------- | | `user_key` | 제휴사가 정하는 회원 식별자입니다. **한 번 정해지면 바뀌지 않고, 회원끼리 겹치지 않아야** 합니다. 내부 회원번호를 권장합니다. | | `item_code` | 제휴사의 상품코드입니다. 구매내역 API 응답과 쏠브 상품을 잇는 키입니다. 파트너 어드민에서는 **연동코드**라고 부릅니다. | | 연동코드 | 파트너 어드민에서 전자책을 등록할 때 입력하는 값으로, `item_code` 와 같습니다. | | 시크릿 | 쏠브가 제휴사 API 를 호출할 때 헤더에 담는 인증 값입니다. **제휴사가 발급**해 쏠브에 전달합니다. | | `platform_type` | 쏠브가 제휴사마다 부여하는 숫자 식별자입니다. ⓑ 방식의 콜백에서만 씁니다. | 아이디·이메일은 회원이 바꿀 수 있습니다. `user_key` 가 바뀌면 회원은 연동과 책장의 책을 모두 잃습니다. 탈퇴한 회원의 값을 다른 회원에게 다시 주는 것도 안 됩니다. ## 로그인 방식 고르기 | | ⓐ 자격증명 로그인 API | ⓑ 호스팅 로그인 페이지 | | ----------------- | ----------------------------------- | -------------------------------- | | 이럴 때 | 자체 ID/PW 로그인만 운영 | 카카오·네이버·구글·Apple 등 소셜 로그인을 함께 운영 | | 제휴사가 만드는 것 | 로그인 API 1개 | 로그인 페이지 + 쏠브 콜백 호출 | | ID/PW 가 쏠브를 거치나요? | 예 (TLS, 로그에 남기지 않음) | 아니요 | | 구현 난이도 | 낮음 | 중간 | | 상세 | [ⓐ 페이지](https://developers.solve.im/docs/partner-integration/login-credential-api) | [ⓑ 페이지](https://developers.solve.im/docs/partner-integration/login-hosted-page) | 소셜로 가입한 회원은 ID/PW 가 없어서 ⓐ 로는 연동할 수 없습니다. 소셜 로그인이 하나라도 있으면 ⓑ 를 고르세요. ## 쏠브가 정하는 것과 제휴사가 정하는 것 | 쏠브가 정합니다 | 제휴사가 정합니다 | | ------------------------------------ | --------------- | | `user_key` 는 불변·유일해야 한다는 규칙 | 로그인 방식 (ⓐ 또는 ⓑ) | | 구매내역 응답 형식 (`item_list[].item_code`) | API 엔드포인트 URL | | ⓑ 콜백 형식 (`id`, `platform_type`) | 인증 헤더 이름과 방식 | | 현재 유효한 구매만 돌려준다는 규칙 | 내부 데이터 구조 | # 상품 연결 (연동코드·카탈로그) URL: https://developers.solve.im/docs/partner-integration/product-catalog > 구매내역 API 의 item_code 를 쏠브 전자책에 연결하는 방법입니다. 파트너 어드민 등록 또는 카탈로그 파일로 진행합니다. 구매내역 API 가 돌려준 `item_code` 가 쏠브의 어떤 전자책인지 알려주는 단계입니다. 방법은 두 가지이고, 제휴사 상황에 맞는 하나를 고르면 됩니다. | 방법 | 이런 제휴사 | 제휴사가 할 일 | | ---------------------- | --------------------------- | ------------------------------------------ | | **A. 파트너 어드민 등록** (권장) | 쏠브 파트너 어드민에 전자책 PDF 를 직접 등록 | 등록할 때 연동코드 입력. 이미 등록한 전자책은 상품 상세에서 한 번 입력. | | **B. 카탈로그 파일** | 쏠브가 전자책을 대신 등록 | 상품 목록 파일 전달, 신상품마다 갱신 | 구매내역 API 가 돌려준 `item_code` 중 연동코드나 카탈로그에 없는 코드는 무시됩니다. 판매 중인 전자책 상품을 빠짐없이 연결해 주세요. ## A. 파트너 어드민에 등록하기 ### 전자책 등록 화면 열기 쏠브 파트너 어드민에서 전자책 PDF 등록을 시작합니다. 어드민 계정은 계약 시 안내해 드립니다. ### 판매유형 고르기 | 판매유형 | 뜻 | | --------- | ---------------------------------------- | | `연동형` | 제휴사 사이트에서 구매한 회원에게만 제공합니다. | | `연동+쏠브북스` | 제휴사 구매 회원에게 제공하고, 쏠브북스에서도 판매합니다. | | `쏠브북스` | 쏠브북스에서만 판매합니다. 연동과 무관하며 연동코드란이 보이지 않습니다. | ### 연동코드 입력 **연동코드**란에 구매내역 API 가 돌려주는 `item_code` 를 그대로 입력합니다. 쏠브는 `item_code` 의 **앞뒤 공백을 떼고** 비교합니다. 그 밖은 대소문자까지 글자 그대로 같아야 합니다. 연동코드 앞뒤에는 공백을 넣지 마세요. 이렇게 등록하면 그 `item_code` 를 구매한 회원의 책장에 책이 자동으로 들어갑니다. **등록 전에 구매한 회원도** 다음 동기화(책장 진입 등) 때 책이 들어갑니다. 카탈로그 파일은 따로 보내지 않아도 됩니다. ### 이미 등록한 전자책에 연동코드 넣기 이미 등록한 전자책은 파트너 어드민의 상품 상세 화면에서 연동코드를 넣을 수 있습니다. * 연동코드가 비어 있을 때 **한 번만** 넣을 수 있습니다. 저장하면 바꿀 수 없으니 제휴사 쇼핑몰의 상품코드와 똑같이 넣어 주세요. * 쏠브북스에서만 판매하던 전자책은 저장하면 판매유형이 `연동+쏠브북스` 로 바뀝니다. * 한 상품코드에 여러 권이 묶인 패키지는 포함된 각 전자책에 같은 연동코드를 넣습니다. 같은 연동코드가 이미 다른 전자책에 들어 있으면 저장할 때 한 번 더 확인합니다. ### 패키지·세트 상품 | 경우 | 입력 방법 | | --------------------------------- | ------------------------------------------------------------ | | 한 상품코드에 여러 권이 묶인 패키지 | 포함된 **각 전자책**을 등록할 때 같은 연동코드를 입력합니다. | | 한 권이 여러 상품코드에 동시에 포함 (단품 + 패키지 등) | 연동코드란에는 코드 하나만 들어가므로, 나머지 코드는 쏠브 담당자에게 알려주세요. 쏠브에서 연결해 드립니다. | | 종이책 + 전자책 혼합 세트 | 전자책을 등록할 때 세트의 상품코드를 입력합니다. | ## B. 카탈로그 파일로 전달하기 쏠브가 전자책을 대신 등록하는 경우, 상품코드가 어떤 책인지 정리한 파일을 보내주세요. 엑셀·CSV·JSON 중 편한 형식이면 됩니다. | 필드 | 필수 | 설명 | | -------------- | -- | -------------------------------------------------------------------------------- | | `item_code` | 필수 | 구매내역 API 의 `item_code` 와 같은 값 | | `item_name` | 필수 | 상품명 | | `isbn_list` | 권장 | 상품에 포함된 도서의 ISBN 목록. 쏠브 전자책과 매칭하는 핵심 키입니다. 패키지는 여러 개, 혼합 세트는 전자책 ISBN 만 적어도 됩니다. | | `product_type` | 선택 | `book`, `package`, `lecture` 등 | | `valid_period` | 선택 | 이용 기간 | ```csv title="catalog.csv" item_code,item_name,product_type,isbn_list BOOK_TOEIC_2026,토익 실전 2026,book,9791234567890 PKG_TOEIC_FULL,토익 풀패키지(LC+RC+해설),package,9791234567890;9791234567891;9791234567892 SET_TOEIC_PAPER_EBOOK,토익 실전 종이+전자 세트,package,9791234567890 ``` ```json title="catalog.json" [ { "item_code": "BOOK_TOEIC_2026", "item_name": "토익 실전 2026", "product_type": "book", "isbn_list": ["9791234567890"] }, { "item_code": "PKG_TOEIC_FULL", "item_name": "토익 풀패키지(LC+RC+해설)", "product_type": "package", "isbn_list": ["9791234567890", "9791234567891", "9791234567892"] } ] ``` CSV 에서 여러 ISBN 은 `;` 로 구분합니다(다른 구분자도 협의 가능). 필드를 더 넣어도 괜찮고, 매칭은 쏠브가 책임지고 진행합니다. * **출시 전**: 판매 중인 전자책 상품 전체를 한 번 보내주세요. * **출시 후**: 신상품이 생길 때마다 해당 행을 추가해 다시 보내주세요. ## item\_code 규칙 (A·B 공통) * **상품 단위**입니다. 책 단위가 아닙니다. 단품 A·B·C 와 이를 묶은 패키지 D 는 각각 다른 코드입니다. * 같은 책이 여러 코드에 들어가도 괜찮습니다. * 한 번 쓴 코드는 **의미를 바꾸거나 다른 상품에 다시 쓰지 마세요.** 과거 구매자의 책장이 엉뚱한 책으로 바뀝니다. ## 관련 문서 * [구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api) · [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding) # 구매내역 API URL: https://developers.solve.im/docs/partner-integration/purchase-history-api > 쏠브가 user_key 를 보내면 회원이 지금 이용할 수 있는 상품코드 목록을 돌려주는, 제휴사가 구현하는 API 입니다. 쏠브가 `user_key` 를 보내면, 그 회원이 **지금 이용할 수 있는** 상품의 `item_code` 목록을 돌려주는 server-to-server API 입니다. 쏠브는 이 목록을 보고 책장에 책을 넣고 뺍니다. 응답에는 **현재 유효한 구매만** 담아 주세요. 목록에서 빠진 상품은 쏠브 책장에서도 빠집니다(목록 전체가 빈 경우는 예외, 아래 표 참고). 구매 이력 전체가 아니라 "지금 이 순간 이용 가능한 것"을 돌려주는 API 입니다. ## 한눈에 보기 | 항목 | 내용 | | ------- | -------------------------------------------------------- | | 호출하는 쪽 | 쏠브 서버 → 제휴사 서버 | | 메서드·URL | `POST`, URL 은 제휴사가 정합니다 | | 형식 | `Content-Type: application/json` 요청, JSON 응답 | | 인증 | 제휴사가 발급한 시크릿을 쏠브가 헤더에 담습니다. 헤더 이름과 방식은 제휴사가 정합니다. | | 타임아웃 | 호출당 10초 | | 재시도 | `5xx`·타임아웃이면 최대 2번 더 호출합니다. `4xx` 는 재시도하지 않습니다. | | 호출 시점 | 연동 직후, 회원의 책장 진입, "전체 동기화" 버튼 ([개념과 용어](https://developers.solve.im/docs/partner-integration/overview)) | | 페이지네이션 | 사용하지 않습니다. 유효한 항목을 한 번에 모두 돌려주세요. | ## 요청 ```bash title="쏠브가 보내는 요청 (curl 로 재현)" curl -X POST https://api.partner.example.com/solve/purchases \ -H 'Content-Type: application/json' \ -H 'X-Solve-Secret: <제휴사가 발급한 시크릿>' \ -d '{"user_key": "100023"}' ``` | 필드 | 타입 | 필수 | 설명 | | ---------- | ------ | -- | --------------- | | `user_key` | string | 필수 | 연동할 때 받은 회원 식별자 | ## 응답 ### 성공 (200) ```json { "item_list": [ { "item_code": "BOOK_TOEIC_2026", "item_name": "토익 실전 2026" }, { "item_code": "PKG_TOEIC_FULL" } ] } ``` | 필드 | 타입 | 필수 | 설명 | | ------------------------- | ---------------- | -- | ----------------------------------------------------------------------------------------- | | `item_list` | array | 필수 | 현재 유효한 구매 항목. 없으면 빈 배열 `[]` | | `item_list[].item_code` | string | 필수 | 제휴사 상품코드. 파트너 어드민에 입력한 **연동코드**와 같은 값이어야 합니다. 책이 아니라 **상품 단위**이며, 한 코드에 여러 권이 들어갈 수 있습니다. | | `item_list[].item_name` | string | 선택 | 표시·문의 대응용 상품명 | | `item_list[].price` | number 또는 string | 선택 | 결제 금액(원). 아래 "선택 필드" 참고 | | `item_list[].paid_at` | string | 선택 | 결제 시각. 아래 "선택 필드" 참고 | | `item_list[].valid_from` | string | 선택 | 이용 시작. 아래 "선택 필드" 참고 | | `item_list[].valid_until` | string | 선택 | 이용 종료. 아래 "선택 필드" 참고 | 위 표에 없는 다른 필드를 더 담아도 쏠브는 무시하므로 괜찮습니다. **응답 본문은 1MB(1,048,576바이트) 이하여야 합니다.** 쏠브는 1MB 를 넘는 응답을 받지 않습니다. 넘으면 그 호출은 실패로 처리하고 재시도하지 않으며, **기존 책장은 유지**합니다. 압축해서 보내는 경우는 푼 뒤의 크기로 셉니다. `item_list` 에는 필요한 필드만 담아 주세요. ### 선택 필드 (v1.1) 항목마다 아래 네 필드를 **선택으로** 더 담을 수 있습니다. 쏠브는 정산과 문의 대응에 사용합니다. | 필드 | 형식 | | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `price` | 0 이상 정수(숫자) 또는 숫자만으로 된 문자열(1\~12자리). 원 단위입니다. | | `paid_at` | ISO 8601 날짜·시각. `Z`·`+09:00` 같은 오프셋이 있으면 그대로 읽고, **오프셋이 없으면 한국 시간(+09:00)** 으로 읽습니다. 날짜만 보내면 그날 00:00 한국 시간입니다. | | `valid_from`, `valid_until` | 1\~32자 문자열. 쏠브는 해석하지 않고 그대로 기록합니다. | * 네 필드 모두 **없어도 동작합니다.** v1.0 대로 구현한 응답은 수정 없이 유효합니다. * 형식이 틀리면 **그 필드만 무시**하고, 책 지급에는 영향이 없습니다. * **`price` 를 안 보내거나 `0` 이면 파트너 어드민에 등록한 가격으로 정산합니다.** * `valid_until` 이 지나도 책은 자동으로 빠지지 않습니다. 책을 회수하려면 지금처럼 **목록에서 빼 주세요.** ```json title="v1.1 예시 — 선택 필드를 모두 채운 항목과 item_code 만 있는 항목" { "item_list": [ { "item_code": "BOOK_TOEIC_2026", "item_name": "토익 실전 2026", "price": 25000, "paid_at": "2026-09-01T14:30:00+09:00", "valid_from": "2026-09-01", "valid_until": "2027-08-31" }, { "item_code": "PKG_TOEIC_FULL" } ] } ``` ### 상황별 응답 **빈 목록과 오류는 결과가 정반대**입니다. 이 표대로 응답해 주세요. | 상황 | 응답 | 쏠브의 처리 | | --------------------- | --------------------------- | ------------------------------------------- | | 구매한 상품이 있음 | `200` + 유효한 항목 | 목록에 있는 책을 책장에 넣습니다 | | 일부 상품이 환불·만료됨 | 해당 항목을 **목록에서 뺌** | 다음 동기화 때 그 책을 책장에서 뺍니다 | | 구매한 상품이 없음 (전부 환불 포함) | `200` + `{"item_list": []}` | 새로 넣을 책이 없습니다. 이미 있는 책은 **빼지 않습니다** (아래 참고) | | 회원이 없음 (탈퇴 등) | `200` + `{"item_list": []}` | 위와 같습니다 | | 시크릿 불일치 | `401` 또는 `403` | 인증 실패로 기록하고 **기존 책장은 유지**합니다 | | 요청 형식 오류 | `400` | 오류로 기록하고 **기존 책장은 유지**합니다 | | 서버 오류, DB 장애, 점검 | `500`·`503` 등 `5xx` | 재시도하고, 실패해도 **기존 책장은 유지**합니다 | 쏠브는 `401` 과 `403` 을 구분하지 않고 같은 인증 실패로 처리합니다. 둘 다 재시도하지 않습니다. **빈 목록일 때 책을 빼지 않는 이유**: 장애 중인 API 가 실수로 빈 목록을 돌려주면 회원 전원의 책이 사라질 수 있어, 쏠브는 목록이 통째로 비어 있으면 회수를 건너뜁니다. 그래서 회원의 **모든** 상품이 환불되거나 회원이 탈퇴한 경우에는 책이 책장에 남습니다. 이런 경우의 회수가 필요하면 쏠브 담당자와 처리 방법을 협의해 주세요. 예외를 삼키고 `200` 을 보내면 쏠브는 정상 응답으로 처리합니다. 특히 여러 저장소 중 일부만 조회된 **불완전한 목록**을 보내면, 빠진 상품이 회원 책장에서 회수됩니다. 조회가 하나라도 실패하면 `5xx` 로 응답해 주세요. ## 구현 예시 시크릿 확인 → 회원의 **현재 유효한** 구매 조회 → `item_list` 응답 순서입니다. 조회 함수는 제휴사 시스템에 맞게 바꾸세요. Node.js Python Java PHP ```js import express from 'express'; import crypto from 'node:crypto'; const app = express(); app.use(express.json()); const SOLVE_SECRET = Buffer.from(process.env.SOLVE_SECRET ?? ''); // 시크릿이 비어 있으면 빈 헤더 요청이 통과하므로 서버를 띄우지 않는다 if (SOLVE_SECRET.length === 0) throw new Error('SOLVE_SECRET is not set'); function isFromSolve(req) { const given = Buffer.from(req.get('X-Solve-Secret') ?? ''); return given.length === SOLVE_SECRET.length && crypto.timingSafeEqual(given, SOLVE_SECRET); } app.post('/solve/purchases', async (req, res) => { if (!isFromSolve(req)) return res.status(403).json({ error: 'forbidden' }); const userKey = req.body?.user_key; if (typeof userKey !== 'string' || userKey === '') { return res.status(400).json({ error: 'invalid_request' }); } try { // 환불·만료를 뺀, 지금 이용 가능한 구매만 조회. 회원이 없으면 빈 배열. const purchases = await findActivePurchases(userKey); return res.json({ // [!code highlight] item_list: purchases.map((p) => ({ item_code: p.productCode, item_name: p.productName })), // [!code highlight] }); // [!code highlight] } catch (err) { // 조회 실패를 200 으로 보내면 불완전한 목록 기준으로 책이 회수될 수 있다 → 반드시 5xx console.error('solve purchases failed', err); return res.status(500).json({ error: 'internal_error' }); // [!code highlight] } }); ``` ```python import hmac import logging import os from fastapi import FastAPI, Header from fastapi.responses import JSONResponse from pydantic import BaseModel app = FastAPI() logger = logging.getLogger(__name__) SOLVE_SECRET = os.environ["SOLVE_SECRET"] class PurchasesRequest(BaseModel): user_key: str @app.post("/solve/purchases") def purchases(body: PurchasesRequest, x_solve_secret: str = Header(default="")): if not hmac.compare_digest(x_solve_secret, SOLVE_SECRET): return JSONResponse({"error": "forbidden"}, status_code=403) try: # 환불·만료를 뺀, 지금 이용 가능한 구매만 조회. 회원이 없으면 빈 리스트. rows = find_active_purchases(body.user_key) except Exception: # 조회 실패를 200 으로 보내면 불완전한 목록 기준으로 책이 회수될 수 있다 → 반드시 5xx logger.exception("solve purchases failed") return JSONResponse({"error": "internal_error"}, status_code=500) # [!code highlight] return {"item_list": [{"item_code": r.product_code, "item_name": r.product_name} for r in rows]} # [!code highlight] ``` ```java @RestController @RequestMapping("/solve") public class SolvePurchaseController { private static final Logger log = LoggerFactory.getLogger(SolvePurchaseController.class); @Value("${solve.secret}") private String solveSecret; private final PurchaseService purchaseService; public SolvePurchaseController(PurchaseService purchaseService) { this.purchaseService = purchaseService; } public record PurchasesRequest(String user_key) {} @PostMapping("/purchases") public ResponseEntity> purchases( @RequestHeader(value = "X-Solve-Secret", defaultValue = "") String secret, @RequestBody PurchasesRequest req) { if (!MessageDigest.isEqual( secret.getBytes(StandardCharsets.UTF_8), solveSecret.getBytes(StandardCharsets.UTF_8))) { return ResponseEntity.status(403).body(Map.of("error", "forbidden")); } if (req.user_key() == null || req.user_key().isBlank()) { return ResponseEntity.badRequest().body(Map.of("error", "invalid_request")); } try { // 환불·만료를 뺀, 지금 이용 가능한 구매만 조회. 회원이 없으면 빈 리스트. List> items = purchaseService.findActive(req.user_key()).stream() .map(p -> Map.of("item_code", p.getProductCode(), "item_name", p.getProductName())) .toList(); return ResponseEntity.ok(Map.of("item_list", items)); // [!code highlight] } catch (Exception e) { // 조회 실패를 200 으로 보내면 불완전한 목록 기준으로 책이 회수될 수 있다 → 반드시 5xx log.error("solve purchases failed", e); return ResponseEntity.status(500).body(Map.of("error", "internal_error")); // [!code highlight] } } } ``` ```php 'forbidden']); exit; } $body = json_decode(file_get_contents('php://input'), true); $userKey = $body['user_key'] ?? null; if (!is_string($userKey) || $userKey === '') { http_response_code(400); echo json_encode(['error' => 'invalid_request']); exit; } try { // 환불·만료를 뺀, 지금 이용 가능한 구매만 조회. 회원이 없으면 빈 배열. $rows = find_active_purchases($userKey); } catch (Throwable $e) { // 조회 실패를 200 으로 보내면 불완전한 목록 기준으로 책이 회수될 수 있다 → 반드시 5xx error_log('solve purchases failed: ' . $e->getMessage()); http_response_code(500); // [!code highlight] echo json_encode(['error' => 'internal_error']); exit; } echo json_encode([ 'item_list' => array_map( fn ($r) => ['item_code' => $r['product_code'], 'item_name' => $r['product_name']], $rows ), ], JSON_UNESCAPED_UNICODE); ``` ## 흐름 ## 구현 체크리스트 * [ ] 환불·만료된 상품을 목록에서 뺀다 * [ ] 구매가 없거나 회원이 없으면 `200` + 빈 배열로 응답한다 * [ ] 조회 실패·장애는 `5xx` 로 응답한다 (빈 배열·불완전한 목록 금지) * [ ] `item_code` 가 파트너 어드민에 입력한 연동코드와 같다 (쏠브는 앞뒤 공백을 떼고 비교하고, 그 밖은 대소문자까지 글자 그대로 같아야 한다) * [ ] 10초 안에 응답한다 * [ ] 구매 항목을 페이지 나누지 않고 한 번에 돌려준다 * [ ] 응답 본문을 1MB(1,048,576바이트) 이하로 만든다 ## 관련 문서 * [상품 연결](https://developers.solve.im/docs/partner-integration/product-catalog) · [보안 요구사항](https://developers.solve.im/docs/partner-integration/security) · [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding) # 구현 순서 URL: https://developers.solve.im/docs/partner-integration/quickstart > 로그인 방식 선택부터 출시까지, 제휴사가 할 일을 5단계로 정리했습니다. 처음 연동하는 제휴사 개발자를 위한 단계별 안내입니다. 각 단계의 상세 규약은 링크된 페이지에 있습니다. [AI로 구현하기](https://developers.solve.im/docs/partner-integration/ai-guide)의 통합 프롬프트를 먼저 복사해 두세요. 아래 단계를 AI 가 같은 기준으로 구현합니다. ### 로그인 방식 고르기 회원 인증 방식에 따라 하나를 고릅니다. 기준은 [개념과 용어](https://developers.solve.im/docs/partner-integration/overview#로그인-방식-고르기)에 있습니다. | 우리 사이트는… | 선택 | | ----------------------------------------------------- | -------------------------------------------- | | 자체 ID/PW 로그인**만** 운영하고, ID/PW 를 검증하는 서버 API 를 만들 수 있다 | [ⓐ 자격증명 로그인 API](https://developers.solve.im/docs/partner-integration/login-credential-api) | | 카카오·네이버·구글·Apple 같은 소셜 로그인을 **함께** 운영한다 | [ⓑ 호스팅 로그인 페이지](https://developers.solve.im/docs/partner-integration/login-hosted-page) | ⓑ 를 고르면 쏠브가 콜백 API URL, `platform_type` 값, 콜백 인증 시크릿을 전달해 드립니다. ### 로그인 구현하기 로그인에 성공하면 회원의 **불변 식별자**(`user_key`)를 쏠브에 넘겨줍니다. 로그인 아이디나 이메일처럼 바뀔 수 있는 값이 아니라, 내부 회원번호처럼 절대 바뀌지 않는 값을 쓰세요. * ⓐ: 쏠브가 보낸 ID/PW 를 검증하고 `{ "user_key": "..." }` 로 응답합니다. * ⓑ: 제휴사 페이지에서 로그인시킨 뒤, 서버에서 쏠브 콜백을 호출하고 받은 `redirect_url` 로 이동시킵니다. ### 구매내역 API 구현하기 쏠브가 `user_key` 를 보내면 **지금 이용할 수 있는** 상품코드 목록을 돌려줍니다. 상세는 [구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api)를 보세요. ```json title="응답 예시" { "item_list": [ { "item_code": "BOOK_TOEIC_2026", "item_name": "토익 실전 2026" } ] } ``` * 환불·만료된 상품은 목록에서 빼주세요. 쏠브 책장에서도 빠집니다. * 회원이 없으면 오류가 아니라 **빈 목록(200)** 으로 응답해 주세요. ### 전자책 등록하기 (연동코드 입력) 쏠브 파트너 어드민에서 전자책 PDF 를 등록할 때 판매유형을 `연동형` 또는 `연동+쏠브북스`로 고르고, **연동코드**란에 구매내역 API 가 돌려주는 `item_code` 와 같은 값을 입력합니다. 그러면 그 상품을 구매한 회원의 책장에 책이 자동으로 들어갑니다. 상세는 [상품 연결](https://developers.solve.im/docs/partner-integration/product-catalog)을 보세요. ### 셀프 점검 후 테스트 자료 전달하기 [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding)의 셀프 점검 스크립트로 먼저 확인한 뒤, 아래 자료를 쏠브에 보내주세요. 구매내역 API 와 ⓐ 로그인 API 는 전체 점검 도구(`solve-selfcheck.mjs`)로, ⓑ 방식의 콜백은 [콜백 흉내 도구](https://developers.solve.im/docs/partner-integration/login-hosted-page)로 미리 시험할 수 있습니다. 쏠브가 실제 호출과 앱 화면으로 검증하고 출시 일정을 잡습니다. * 로그인 URL(ⓐ API 또는 ⓑ 로그인 페이지)과 구매내역 API URL * 구매 내역이 있는 테스트 계정 * 그 계정이 보유한 `item_code` 와 상품명 ## 다음 단계 * 구매한 회원을 쏠브 앱으로 안내하는 방법: [구매 후 사용자 안내](https://developers.solve.im/docs/partner-integration/user-discovery) * 자주 묻는 질문: [FAQ](https://developers.solve.im/docs/partner-integration/faq) # 보안 요구사항 URL: https://developers.solve.im/docs/partner-integration/security > 전송·인증·식별자·세션 등 연동 전 구간에 적용되는 보안 항목입니다. 연동의 모든 구간에 적용되는 보안 항목입니다. 출시 전에 꼭 확인해 주세요. | 항목 | 요구사항 | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 전송 | 모든 API·콜백·페이지는 HTTPS(TLS)만 사용합니다. | | 서버 간 인증 | 쏠브는 **제휴사가 발급한 시크릿**을 헤더에 담아 호출합니다. 제휴사는 상수 시간 비교로 확인하고, 쏠브는 시크릿을 안전하게 보관하며 로그에 남기지 않습니다. | | `user_key` | 한 번 정한 값은 절대 바뀌지 않고, 회원끼리 겹치지 않아야 합니다. 탈퇴 회원의 값을 다시 쓰면 안 됩니다. | | 자격증명 | ⓐ 에서는 쏠브가 ID/PW 를 TLS 서버 간 호출로만 전달하고 로그에 남기지 않습니다. ⓑ 에서는 ID/PW 가 쏠브로 전달되지 않습니다. | | ⓑ 콜백 | 로그인에 **성공한 경우에만** 제휴사 **서버**에서, 쏠브가 발급한 콜백 인증 시크릿(`Authorization: Bearer`)을 담아 호출합니다. | | ⓑ 세션 | 쏠브는 연동을 시작한 웹뷰에 1회용 토큰을 발급해 재생 공격·CSRF 를 막고, `redirect_url` 에 만료 시간이 있는 서명을 담아 회원 식별자 위변조를 막습니다. `redirect_url` 은 수정하지 말고 같은 웹뷰에서 여세요. 제휴사가 `state`·nonce 를 따로 구현할 필요는 없습니다. | | 호출량 | rate limit 은 두지 않아도 됩니다. 둔다면 **분당 600회(초당 10회) 이상**으로 잡아 주세요. 쏠브는 책장 진입·전체 동기화 때 몰아서 호출하고, 일시 장애 때 재시도합니다. | | 시크릿 회전 | **제휴사가 발급한 API 시크릿**: 바꾸실 때는 미리 알려 주세요. **새 값과 옛 값을 함께 받아 주시는 동안** 쏠브가 새 값으로 바꿉니다(반영까지 최대 5분). 바뀐 것을 확인하신 뒤 옛 값을 폐기하시면 됩니다.
**쏠브가 발급한 콜백 인증 시크릿**: 쏠브가 새 값을 드리고, **일정 기간 새 값과 옛 값을 함께 받습니다.** 그 기간 안에 새 값으로 바꿔 주세요. | ## 같은 제휴사 계정을 다른 쏠브 계정에 연동하면 이미 다른 쏠브 사용자에게 연동된 제휴사 회원이 새로 연동하면, 이전 연동을 해제하고 새 사용자에게 옮깁니다. 제휴사는 `user_key` 의 불변·유일성만 지키면 되고, 이전 처리는 쏠브가 합니다. ## 관련 문서 * [ⓐ 자격증명 로그인 API](https://developers.solve.im/docs/partner-integration/login-credential-api) · [ⓑ 호스팅 로그인 페이지](https://developers.solve.im/docs/partner-integration/login-hosted-page) · [구매내역 API](https://developers.solve.im/docs/partner-integration/purchase-history-api) # 구매 후 사용자 안내 URL: https://developers.solve.im/docs/partner-integration/user-discovery > 구매한 회원이 쏠브 앱에 도달하도록 안내하는 방법 # 구매 후 사용자 안내 이 연동은 **사용자가 쏠브 앱 안에서 직접 제휴사 계정을 연동**하는 모델입니다. 제휴사 사이트에서 결제가 끝난 뒤의 다음 단계는 사용자가 쏠브 앱에서 진행하므로, 회원이 쏠브 앱의 존재를 인지하도록 안내해 주시면 실제 연동률이 크게 올라갑니다. > 이 연동 모델에서 **쏠브는 구매 회원에게 문자(SMS)를 발송하지 않습니다.** 쏠브가 주문 정보를 직접 수신하는 일부 커머스 채널과 달리, 이 모델은 사용자가 앱에서 스스로 연동을 시작하는 구조이기 때문입니다. 제휴사 측의 SMS 자동화도 필수가 아닙니다. ## 사용자가 구매 후 책을 보는 흐름 1. 제휴사 사이트에서 전자책·강의를 구매합니다. 2. 쏠브 앱을 실행합니다 (없으면 앱스토어에서 설치, 최초 1회). 3. 앱 안에서 "제휴사 연동하기"로 진입합니다. 4. 제휴사 계정으로 로그인하면 연동이 완료됩니다. 5. 쏠브가 구매내역 API 를 자동으로 호출합니다 (백그라운드). 6. 책장에 구매한 전자책이 자동 등록·표시됩니다. 7. 책을 선택해 학습을 시작합니다. ## 안내 방법 아래 두 옵션은 모두 선택 사항이며, 필수 구현 범위에 포함되지 않습니다. 하나만 선택하셔도 되고 병행하셔도 됩니다. ### 옵션 A. 제휴사 사이트 내 안내 (권장) 사이트 안에 "쏠브 앱에서 학습 가능" 안내와 진입점(버튼·배너)을 노출하는 방식입니다. 별도 시스템 구축 없이 사용자가 자연스럽게 인지할 수 있습니다. * **권장 위치** — 결제 완료 페이지가 가장 효과적입니다. 마이페이지·구매내역 페이지도 좋습니다. * **선택 위치** — 전자책 상품 상세 페이지의 안내 배지. * 쏠브가 알려 드리는 웹 연동 주소를 걸어 두면, 클릭 시 쏠브북스 웹의 "제휴사 연동" 화면이 열립니다. 쏠브북스에 로그인하면 연동 창이 바로 뜹니다. 주소 형식은 `https://books.solve.im/mypage/partner-connect?partner=<제휴사 이름>` 이고, `partner` 값은 계약 시 알려 드립니다. ### 옵션 B. 제휴사 자체 알림 채널 (선택) 기존에 운영 중인 알림톡·이메일 등에 안내 문구를 추가하는 방식입니다. 쏠브가 강제하는 형식은 없으며, 아래 요소를 포함해 주시면 좋습니다. * 구매한 상품을 쏠브 앱에서 학습할 수 있다는 안내와 앱 다운로드 링크 * 앱 실행 후 "제휴사 연동하기"에서 로그인하면 책장에 자동 추가된다는 안내 예시 문구 — "구매하신 \[상품명]은 쏠브 앱에서 학습하실 수 있습니다. \[앱 다운로드 링크] 앱 실행 후 '제휴사 연동'에서 로그인하시면 책장에 자동으로 추가됩니다." ## 제공 자료 아래 자료는 계약 시 쏠브 담당자가 별도로 전달합니다. * 웹 연동 주소 — 클릭 시 쏠브북스 웹의 제휴사 연동 화면이 열립니다 (쏠브북스 로그인 필요) * 앱 설치 링크 — 클릭 시 스토어로 이동 * BI 가이드 — 로고 파일, 권장 카피, 색상·버튼 규격 (제휴사 사이트의 톤앤매너에 맞춰 조정하셔도 무방합니다) ## 관련 문서 * [제휴사 연동 개요](https://developers.solve.im/docs/partner-integration) · [테스트와 출시](https://developers.solve.im/docs/partner-integration/onboarding)