레시피 / 회원 가입·로그인
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.kr 의 site/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"
}
성공 201 — Member 예:
{
"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}= 위 단계에서 받은 D1idlocal_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|auth 의 requestable: true. 2. client_credentials로 위 스코프 토큰 발급. 3. POST /members → id 수신 → POST /members/{id}/link → GET /members/{id} 에 local_member_id 표시. 4. 잘못된 비밀번호로는 당신 서버가 401을 내고 Partner auth/token을 호출하지 않음. 5. 올바른 비밀번호 후 auth/token → member_store=hybrid JWT. 6. link 없이 auth/token → 403 hybrid_link_missing.
---
9. 참고 링크
- 빠른 시작: /docs
- 토큰: /recipes/00-token-refresh
- 콘솔: /console
- OpenAPI: /page/partner_openapi
---
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_missingUX(재링크 또는 재가입 안내)가 있다.