레시피 / 회원 가입·로그인

Recipe — 회원 가입·로그인 (Partner API)

대상 독자: 외부 사이트·다른 AI 에이전트가 https://api.itjeom.com/partner/v1 만으로 회원가입·로그인을 구현할 때 공개 URL: https://developers.itjeom.com/recipes/03-member-signup-login

---

0. AI 에이전트용 한 줄 지시

1. 작업 디렉터리 = 빈 폴더 DocumentRoot. 예: e:\itjeom_com\htdocs\site\sub\developers-test 또는 관리자 「API 테스트 만들기」로 만든 {slug}.ilw.krsite/sub/{slug}/. - EGB 엔진·theme robocopy·스토어 프로비저닝 복제 금지. 엔진 ZIP/테마를 긁어오지 말 것. - 빈 폴더에 당신 스택(Node/PHP plain/정적+백엔드 등)으로 가입·로그인 UI·API를 처음부터 만든다. 2. 베이스 URL은 https://api.itjeom.com/partner/v1 만 사용한다. 3. client_secret·Partner 토큰·회원 비밀번호는 서버(백엔드)에서만 다룬다. 4. Partner members API는 D1 비PII(등급·스키마·로컬 ID 연결) 만 다룬다. 이메일·비밀번호·이름·전화·status는 Partner에 보내지 않는다 — 그 값은 당신 앱이 빈 폴더 안에서 둔 DB/파일에 저장한다. 5. 허브 로그인(/itjeom-hub/*, itjeom_users)·스토어 엔진 내부 /shop/*·관리자 JWT·EGB egb_themes/theme 복사쓰지 않는다. 6. 필요 스코프: member:read member:write member:auth (+ 토큰 발급용 앱 승인). 앱 신청 도메인 = 이 빈 호스트 FQDN(예: developers-test.ilw.kr). - 콘솔 신청 → DNS TXT _itjeom-partner.{domain} 소유 확인 → pending → 마스터 승인. (남의 도메인 가로채기 방지) 7. 완료 기준은 아래 §10 Acceptance 체크리스트를 모두 통과하는 것이다.

스펙 전체: OpenAPI · YAML /openapi/partner-v1.yaml · 스코프 목록 GET /meta/scopes.

로컬 빈 호스트 만들기 (참고)

  • 관리자: 잇점 관리 → 테마 배포 → 「API 테스트 만들기」 (mode=api_blank) → site/sub/{slug}/ 빈 폴더 + CF A + partner_api 호스트.
  • 또는 이미 있는 예: e:\itjeom_com\htdocs\site\sub\developers-test (파일 0개 DocumentRoot).
  • 「테스트 스토어 만들기」는 쓰지 말 것 — 그건 EGB theme robocopy다.
  • 같은 FQDN으로 developers 콘솔에서 신청 → TXT 소유 확인(ilw.kr Zone이면 Worker가 TXT 자동 upsert 가능) → 승인 후 client_id/secret.

---

1. 역할 분리 (반드시 이해)

| 계층 | 저장·책임 | Partner로 하는 일 | |------|-----------|-------------------| | 당신 앱 서버 | 이메일·비밀번호 해시·표시 이름·세션 쿠키 등 PII·계정 상태 | 비밀번호 검증 Partner 호출 | | Partner API (/partner/v1/members*) | D1 itjeom_tenant_members 비PII | 그림자 회원 생성·등급/스키마·local_member_id 연결·하이브리드 JWT 발급 | | 잇점 허브 | itjeom.com 허브 회원 | 이 레시피에서 사용 금지 |

식별 관계:

  • Partner 응답 id = D1 PK = JWT의 tenant_member_id
  • local_member_id = 당신 앱 DB의 회원 PK(또는 스토어 MySQL PK). Partner가 비밀번호를 검증하지 않음.
[브라우저/앱] → [당신 서버: 이메일·비번 검증]
                      ↓ (검증 성공 후만)
                 [Partner: /members/auth/token]
                      ↓
                 [하이브리드 JWT 반환] → 당신 서버가 세션 발급

---

2. 사전 조건

1. developers.itjeom.com/console에서 API 사용 도메인으로 앱 신청. 2. 스코프에 member:read · member:write · member:auth 포함 후 마스터 승인. 3. 발급된 client_id / client_secret을 서버 env에만 보관. 4. (선행) 00-token-refresh로 Partner 앱 access_token 발급 가능해야 함.

POST /partner/v1/oauth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "scope": "member:read member:write member:auth"
}

이후 members 호출마다:

Authorization: Bearer {partner_access_token}

---

3. 회원가입 구현 (권장 순서)

3.1 당신 서버에서 할 일 (필수)

1. 이메일·비밀번호 입력 검증(길이·형식·약관 등 — 앱 정책). 2. 당신 DB에 회원 INSERT → local_member_id(자동증가 PK) 확보. 3. 비밀번호는 당신 DB에 해시로만 저장. Partner에 비밀번호를 POST하지 말 것.

3.2 Partner: D1 그림자 회원 생성

POST /partner/v1/members
Authorization: Bearer {partner_access_token}
Content-Type: application/json

{
  "user_level": "0",
  "signup_schema_slug": "default"
}

성공 201Member 예:

{
  "id": 12345,
  "local_member_id": null,
  "user_level": "0",
  "signup_schema_slug": "default",
  "referral_code": null,
  "referrer_type": null,
  "referrer_ref": null,
  "created_at": "...",
  "updated_at": "..."
}

당신 DB에 partner_tenant_member_id = id 를 같이 저장해 두는 것을 권장한다.

3.3 Partner: 로컬 PK 연결

POST /partner/v1/members/{id}/link
Authorization: Bearer {partner_access_token}
Content-Type: application/json

{
  "local_member_id": 1001
}
  • {id} = 위 단계에서 받은 D1 id
  • local_member_id = 당신 DB PK
  • 이미 다른 local_member_id에 연결돼 있으면 409 already_linked

3.4 (선택) 회원가입 직후 로그인 세션

§4와 동일하게 POST /members/auth/token으로 하이브리드 JWT를 받은 뒤, 당신 서버 세션을 만든다. 브라우저에 Partner client_secret이나 Partner 앱 토큰을 내려주지 말 것.

---

4. 로그인 구현 (권장 순서)

4.1 당신 서버에서 할 일 (필수)

1. 이메일(또는 로그인 ID)로 당신 DB에서 회원 조회. 2. 비밀번호 검증. 실패 시 Partner를 호출하지 않는다. 3. 해당 행의 local_member_id(또는 저장해 둔 partner_tenant_member_id) 확보.

4.2 Partner: 하이브리드 JWT 발급

POST /partner/v1/members/auth/token
Authorization: Bearer {partner_access_token}
Content-Type: application/json

{
  "local_member_id": 1001
}

또는 (링크가 이미 있다면):

{ "id": 12345 }

성공 200 예:

{
  "access_token": "<tenant_hybrid_jwt>",
  "token_type": "Bearer",
  "tenant_member_id": 12345,
  "local_member_id": 1001,
  "member_store": "hybrid"
}

4.3 당신 서버 세션

  • 위 JWT를 서버 세션/쿠키(HttpOnly) 에만 저장하거나, 당신 앱 전용 세션 ID만 발급하고 JWT는 서버 vault에 둔다.
  • 이 JWT의 claim 요지: principal=tenant_member, tenant_member_id, site_id, member_store=hybrid, local_member_id.
  • 허브 회원 JWT가 아니다.

4.4 오류

| HTTP | code (대략) | 의미 | 조치 | |------|-------------|------|------| | 403 | hybrid_link_missing | D1에 local_member_id 미연결 | 가입 시 §3.3 link 재실행 | | 404 | not_found | 회원 없음 | local_member_id/id 확인 | | 401/403 | scope | Partner 앱 토큰·스코프 부족 | member:auth 포함해 재발급 |

---

5. 조회·메타 (가입 후 UI용)

목록:

GET /partner/v1/members?page=1&per_page=20
Authorization: Bearer {partner_access_token}

단건:

GET /partner/v1/members/{id}

배치 메타(등급·스키마만, 최대 200 ids):

GET /partner/v1/members/meta?ids=12345,12346

등급·스키마만 바꿀 때:

PATCH /partner/v1/members/{id}
Content-Type: application/json

{ "user_level": "1", "signup_schema_slug": "default" }

응답에 email / password / name / phone / status 필드는 없다 — 필요하면 당신 DB에서 조합한다.

---

6. 구현 의사코드 (서버)

function signup(email, password, profile):
  assert_valid(email, password)
  local_id = db.insert_user(email, hash(password), profile)   // PII here only
  partner_tok = partner_oauth_client_credentials(
    scopes=["member:write", "member:auth", "member:read"])
  member = http.POST("/partner/v1/members", auth=partner_tok, body={user_level:"0"})
  http.POST("/partner/v1/members/" + member.id + "/link",
            auth=partner_tok, body={local_member_id: local_id})
  db.set_partner_member_id(local_id, member.id)
  return issue_app_session(optional partner_auth_token(local_id))

function login(email, password):
  user = db.find_by_email(email)
  if not verify(password, user.password_hash): return 401
  partner_tok = partner_oauth_client_credentials(scopes=["member:auth"])
  hybrid = http.POST("/partner/v1/members/auth/token",
                     auth=partner_tok, body={local_member_id: user.id})
  return issue_app_session(hybrid.access_token, user)

---

7. 금지 사항 (에이전트·구현자)

  • EGB theme / 엔진 ZIP / 「테스트 스토어 만들기」 robocopy로 DocumentRoot를 채우는 것 — API 테스트는 빈 폴더만 (developers-test 패턴)
  • Partner API에 email / password / name / phone 전송
  • client_secret·Partner 앱 토큰을 프론트엔드·모바일 앱에 하드코딩
  • https://api.itjeom.com/itjeom-hub/* 또는 스토어 내부 /register·admin API로 “대신” 구현
  • 비밀번호 검증 없이 /members/auth/token만 호출해 임의 사용자로 로그인시키는 것
  • Partner members 응답을 “전체 회원 프로필”로 UI에 표시(PII는 당신 DB)

---

8. 테스트 체크리스트 (수동 / CI)

1. GET /meta/scopes 에서 member:read|write|authrequestable: true. 2. client_credentials로 위 스코프 토큰 발급. 3. POST /membersid 수신 → POST /members/{id}/linkGET /members/{id}local_member_id 표시. 4. 잘못된 비밀번호로는 당신 서버가 401을 내고 Partner auth/token호출하지 않음. 5. 올바른 비밀번호 후 auth/tokenmember_store=hybrid JWT. 6. link 없이 auth/token403 hybrid_link_missing.

---

9. 참고 링크

---

10. Acceptance (구현 완료 판정)

다른 AI·개발자가 “끝났다”고 보고하려면 아래를 모두 충족해야 한다.

  • [ ] DocumentRoot가 빈 폴더에서 시작한 자체 앱이다(EGB theme robocopy 아님 · developers-test 패턴).
  • [ ] 콘솔에서 도메인 신청 → DNS TXT(_itjeom-partner) 소유 확인 → 마스터 승인까지 완료했다.
  • [ ] 가입: 앱 DB INSERT + POST /members + POST /members/{id}/link 순서가 코드에 존재한다.
  • [ ] 로그인: 앱 DB 비밀번호 검증 성공 후에만 POST /members/auth/token을 호출한다.
  • [ ] 시크릿·Partner 토큰이 브라우저/공개 repo에 없다.
  • [ ] Partner members 경로에 PII 필드가 없다.
  • [ ] /itjeom-hub·엔진 내부 회원 API·EGB 엔진 복사를 쓰지 않는다.
  • [ ] 에러 hybrid_link_missing UX(재링크 또는 재가입 안내)가 있다.
처리 중...
잠시만 기다려주세요