1차 보완(0.1.1+14) 내용 합치기
This commit is contained in:
@@ -26,7 +26,7 @@ export const fnAuthOnCreate = functionsV1.region('asia-northeast3').auth.user().
|
||||
const walletRef = db.doc(`users/${user.uid}/wallet/main`);
|
||||
// 이메일 — 10/5 「플랫폼이 이메일 제공을 승인·동의한 경우에만 수집, 없어도 가입·로그인 가능」.
|
||||
// Auth 계정 이메일이 비면(OIDC 등) 제공자 정보의 이메일을 쓴다. 카카오(커스텀 토큰)는
|
||||
// 계정을 만든 뒤에 연결 정보가 붙으므로 `resolveKakaoUid` 가 따로 쓴다.
|
||||
// 계정을 만든 뒤에 연결 정보가 붙으므로 `exchangeSocial`(social_token.ts)이 따로 쓴다.
|
||||
const email = user.email ?? user.providerData.find((p) => p.email)?.email ?? null;
|
||||
const defaults: Record<string, unknown> = {
|
||||
uid: user.uid,
|
||||
@@ -436,36 +436,73 @@ export const fnPlanetCreate = onCall(async (req) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* `fn-consent-record` — 약관 동의 이력.
|
||||
* 가입 이후 따로 받는 동의 — 위치기반 서비스(Q-E1 · 1차 검수 R02 「위치기능 최초 사용 전」)와 마케팅 수신.
|
||||
* 가입 필수 항목(서비스 · 개인정보 수집·이용 · 만 14세)은 여기서 철회하지 않는다(탈퇴로만 끝난다).
|
||||
*/
|
||||
export const LATER_CONSENTS = ['location', 'marketing'] as const;
|
||||
|
||||
/**
|
||||
* `fn-consent-record` — 약관 동의 · 철회 이력.
|
||||
*
|
||||
* append-only 다. 철회도 기존 행을 고치지 않고 `granted: false` 인 새 행으로 남긴다.
|
||||
* append-only 다. 철회도 기존 행을 고치지 않고 `granted: false` 인 새 행으로 남긴다 — 행 ID 는
|
||||
* `{항목}_{버전}_{grant|revoke}_{시각}` 이라 같은 항목을 동의 → 철회 → 재동의해도 앞 행이 덮이지 않는다.
|
||||
* 동의 시점을 되짚을 수 없으면 분쟁에서 증명할 수단이 사라진다.
|
||||
*
|
||||
* 지금 상태는 사용자 문서 `consentState.{항목}`(서버만 쓴다 — 규칙의 본인 수정 목록에 없음)에 함께 둔다.
|
||||
* 위치 Quest 인증(`fnQuestVerifyPlace`)이 이 값으로 동의 여부를 본다.
|
||||
*/
|
||||
export const fnConsentRecord = onCall(async (req) => {
|
||||
const uid = callerUid(req);
|
||||
const docType = str(req.data, 'docType');
|
||||
const docVersion = str(req.data, 'docVersion');
|
||||
if (!(LATER_CONSENTS as readonly string[]).includes(docType)) {
|
||||
throw new HttpsError('invalid-argument', '기록할 수 없는 동의 항목입니다.');
|
||||
}
|
||||
const granted = (req.data as Record<string, unknown>)?.granted === true;
|
||||
const consentId = `${docType}_${docVersion}_${granted ? 'grant' : 'revoke'}`;
|
||||
|
||||
await getFirestore().doc(`users/${uid}/consents/${consentId}`).set({
|
||||
const consentId = `${docType}_${docVersion}_${granted ? 'grant' : 'revoke'}_${Date.now()}`;
|
||||
const db = getFirestore();
|
||||
const batch = db.batch();
|
||||
batch.set(db.doc(`users/${uid}/consents/${consentId}`), {
|
||||
docType,
|
||||
docVersion,
|
||||
granted,
|
||||
ip: req.rawRequest?.ip ?? null,
|
||||
userAgent: req.rawRequest?.headers?.['user-agent'] ?? null,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
});
|
||||
batch.set(db.doc(`users/${uid}`), {
|
||||
consentState: { [docType]: { granted, version: docVersion, at: FieldValue.serverTimestamp() } },
|
||||
// 마케팅은 Push Marketing 구분과 같은 값이다(가입 때와 같은 규칙).
|
||||
...(docType === 'marketing' ? { notificationPrefs: { marketing: granted } } : {}),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
}, { merge: true });
|
||||
|
||||
return { consentId };
|
||||
await batch.commit();
|
||||
return { consentId, granted };
|
||||
});
|
||||
|
||||
/**
|
||||
* 가입 약관 필수 항목 — Figma `179:24230`(서비스 이용약관 · 위치기반 서비스 · 만 14세 이상)에
|
||||
* 10/3 통합회신 3절 4 · 10/5 「개인정보처리방침을 약관과 함께 전달」의 개인정보 처리방침을 더했다.
|
||||
* 항목 구성은 해석 확인 요청 E-Q1(회신 대기). 마케팅 수신은 선택 · 기본 OFF(별첨 B 「Push 알림」).
|
||||
* 위치기반 서비스에 동의한 상태인가. `consentState.location` 이 있으면 그 값(철회 포함)을 따르고,
|
||||
* 없으면 예전 가입 화면(위치 약관이 가입 필수였던 0.2.0 이전)에서 남긴 `location_{가입 약관 버전}_grant`
|
||||
* 행으로 본다 — 이미 동의한 사용자에게 다시 묻지 않는다.
|
||||
*/
|
||||
export const SIGNUP_REQUIRED_TERMS = ['service', 'privacy', 'location', 'age'] as const;
|
||||
export async function hasLocationConsent(uid: string): Promise<boolean> {
|
||||
const db = getFirestore();
|
||||
const user = (await db.doc(`users/${uid}`).get()).data();
|
||||
const state = (user?.consentState as Record<string, { granted?: unknown }> | undefined)?.location;
|
||||
if (state) return state.granted === true;
|
||||
const version = user?.termsVersion;
|
||||
if (typeof version !== 'string' || !version) return false;
|
||||
return (await db.doc(`users/${uid}/consents/location_${version}_grant`).get()).exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* 가입 약관 필수 항목 — 통합검토회신 v2.3 **Q-E1** · 1차 검수 회신서 **R02**.
|
||||
*
|
||||
* 서비스 이용약관 · 개인정보 수집·이용 동의 · 만 14세 이상 확인만 가입 필수다. **위치기반 서비스 약관은
|
||||
* 가입 때 받지 않는다** — 위치기능을 처음 쓰기 직전에 따로 받는다([fnConsentRecord] `location`).
|
||||
* 개인정보처리방침은 동의 항목이 아니라 고지(링크)라 기록하지 않는다. 마케팅 수신은 선택 · 기본 OFF.
|
||||
*/
|
||||
export const SIGNUP_REQUIRED_TERMS = ['service', 'privacy', 'age'] as const;
|
||||
|
||||
/**
|
||||
* `fn-signup-consent` — 가입 약관 동의를 한 번에 기록한다(FR-CMN-04 · 원장 C-134).
|
||||
@@ -474,6 +511,8 @@ export const SIGNUP_REQUIRED_TERMS = ['service', 'privacy', 'location', 'age'] a
|
||||
* 찍는다. 앱은 이 값이 없으면 로그인 직후 온보딩(약관부터)으로 보낸다 — 「로그인」 버튼으로 처음
|
||||
* 들어온 소셜 계정(카카오 커스텀 토큰 포함)이 약관 · 만 14세 확인 없이 지나가지 못하게 하는 기준이다.
|
||||
* 마케팅 동의는 Push Marketing 구분(`notificationPrefs.marketing`)과 같은 값으로 둔다.
|
||||
* 예전 앱(0.2.0+10 이하)은 가입 화면에서 위치 약관을 받아 `location` 을 함께 보낸다. 사용자가 실제로
|
||||
* 동의한 것이라 그대로 기록한다 — 버리면 그 앱으로 가입한 계정은 위치 동의 화면이 없어 위치 Quest 를 못 한다.
|
||||
*/
|
||||
export const fnSignupConsent = onCall(async (req) => {
|
||||
const uid = callerUid(req);
|
||||
@@ -497,10 +536,19 @@ export const fnSignupConsent = onCall(async (req) => {
|
||||
batch.set(db.doc(`users/${uid}/consents/${docType}_${version}_${granted ? 'grant' : 'revoke'}`),
|
||||
{ docType, granted, ...meta }, { merge: true });
|
||||
}
|
||||
const legacyLocation = agreed.has('location');
|
||||
if (legacyLocation) {
|
||||
batch.set(db.doc(`users/${uid}/consents/location_${version}_grant`),
|
||||
{ docType: 'location', granted: true, ...meta }, { merge: true });
|
||||
}
|
||||
batch.set(db.doc(`users/${uid}`), {
|
||||
termsAgreedAt: FieldValue.serverTimestamp(),
|
||||
termsVersion: version,
|
||||
notificationPrefs: { marketing },
|
||||
consentState: {
|
||||
marketing: { granted: marketing, version, at: FieldValue.serverTimestamp() },
|
||||
...(legacyLocation ? { location: { granted: true, version, at: FieldValue.serverTimestamp() } } : {}),
|
||||
},
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
}, { merge: true });
|
||||
await batch.commit();
|
||||
|
||||
Reference in New Issue
Block a user