가입 시 약관 동의 단계 추가 — 필수·선택 항목 구분 · 개인정보처리방침 고지 · 동의 기록 저장

This commit is contained in:
Sn-Kinos
2026-10-11 23:43:25 +09:00
parent 9119b3beaa
commit c40bde2cd0
9 changed files with 564 additions and 51 deletions
+52 -1
View File
@@ -1,5 +1,5 @@
import * as functionsV1 from 'firebase-functions/v1';
import { onCall } from 'firebase-functions/v2/https';
import { HttpsError, onCall } from 'firebase-functions/v2/https';
import { getFirestore, FieldValue } from 'firebase-admin/firestore';
import { getAuth } from 'firebase-admin/auth';
@@ -85,6 +85,57 @@ export const fnConsentRecord = onCall(async (req) => {
return { consentId };
});
/**
* 가입 약관 필수 항목 — 서비스 이용약관 · 개인정보 수집·이용 동의 · 만 14세 이상 확인.
*
* 위치기반 서비스 약관은 가입 때 받지 않는다(위치기능을 처음 쓰기 직전에 따로 받는다).
* 개인정보처리방침은 동의 항목이 아니라 고지(링크)라 기록하지 않는다. 마케팅 수신은 선택 · 기본 OFF.
*/
export const SIGNUP_REQUIRED_TERMS = ['service', 'privacy', 'age'] as const;
/**
* `fn-signup-consent` — 가입 약관 동의를 한 번에 기록한다.
*
* 항목마다 `consents` 에 append-only 행을 남기고(약관 버전 포함), 사용자 문서에 `termsAgreedAt` 을
* 찍는다. 앱은 이 값이 없으면 로그인 직후 약관 화면으로 보낸다 — 가입 · 로그인 버튼이나 제공자와
* 무관하게 약관 · 만 14세 확인 없이 지나가지 못하게 하는 기준이다.
* 마케팅 동의는 알림 설정의 마케팅 구분(`notificationPrefs.marketing`)과 같은 값으로 둔다.
*/
export const fnSignupConsent = onCall(async (req) => {
const uid = callerUid(req);
const version = str(req.data, 'version');
const raw = (req.data as Record<string, unknown>)?.agreed;
const agreed = new Set(Array.isArray(raw) ? raw.filter((v): v is string => typeof v === 'string') : []);
if (!SIGNUP_REQUIRED_TERMS.every((t) => agreed.has(t))) {
throw new HttpsError('invalid-argument', '필수 약관에 모두 동의해 주세요.');
}
const marketing = agreed.has('marketing');
const db = getFirestore();
const batch = db.batch();
const meta = {
docVersion: version,
ip: req.rawRequest?.ip ?? null,
userAgent: req.rawRequest?.headers?.['user-agent'] ?? null,
createdAt: FieldValue.serverTimestamp(),
};
for (const docType of [...SIGNUP_REQUIRED_TERMS, 'marketing']) {
const granted = docType === 'marketing' ? marketing : true;
batch.set(db.doc(`users/${uid}/consents/${docType}_${version}_${granted ? 'grant' : 'revoke'}`),
{ docType, granted, ...meta }, { merge: true });
}
batch.set(db.doc(`users/${uid}`), {
termsAgreedAt: FieldValue.serverTimestamp(),
termsVersion: version,
notificationPrefs: { marketing },
consentState: {
marketing: { granted: marketing, version, at: FieldValue.serverTimestamp() },
},
updatedAt: FieldValue.serverTimestamp(),
}, { merge: true });
await batch.commit();
return { termsVersion: version, marketing };
});
/**
* `fn-admin-set-role` — 마스터 전용. 운영자 등급을 커스텀 클레임으로 심는다.
*
+1 -1
View File
@@ -4,7 +4,7 @@
import './core/bootstrap.js';
export {
fnAuthOnCreate, fnConsentRecord, fnAdminSetRole,
fnAuthOnCreate, fnConsentRecord, fnSignupConsent, fnAdminSetRole,
} from './account/account.js';
export { fnAuthKakao } from './account/kakao.js';
export { fnAuthLine } from './account/line.js';