사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.











usePushNotification() 하나와 서버의 발송기 둘로 이루어집니다. iOS는 APNs, Android와 웹은 FCM입니다. 발송기 인증 정보가 없는 토큰은 로그 한 줄만 남기고 건너뛰니, 아래 표에서 해당하는 줄을 빠짐없이 준비하세요.register()가 이 주소로 배달하는 provider와 함께 돌려줍니다.apns, Android와 웹은 fcm입니다. 서버는 이 값으로 발송기를 고릅니다.vapidKey에 넣습니다..p8 키입니다. 키 하나로 두 APNs 환경을 모두 씁니다.firebase 아래에 공개 Firebase 웹 설정과 vapidKey를 넣습니다.akan.config.ts의 native.android.googleServices로 지정합니다.akan.config.ts의 native에 적습니다.env.server.*에 둡니다. 서버가 FCM에 발송할 때 쓰는 서비스 계정입니다.env.server.*에 둡니다. APNs 키와 Key ID, Team ID, 앱의 bundle id입니다.push 플러그인이 맡고, native의 permissions: ["push"]만으로 들어갑니다. 설치할 패키지는 없습니다. 플러그인은 플랫폼마다 그 플랫폼의 서비스를 씁니다:push.register() → { provider: "apns" }google-services.json을 직접 읽으므로 Gradle 플러그인이 필요 없습니다.push.register() → { provider: "fcm" }usePushNotification()이 그 차이를 감춥니다. 네이티브 셸에서는 플러그인을, 브라우저에서는 Firebase를 부르고, 어느 쪽이든 같은 모양의 PushToken을 돌려줍니다. 어떤 권한이 어떤 플러그인을 넣는지는 설정 문서에서 다룹니다.env.client.*의 firebase 아래에 넣습니다.vapidKey에 넣습니다.env.client.*는 브라우저로 전달됩니다. 서버의 서비스 계정은 env.server.*에 둡니다.apiKey, projectId, messagingSenderId, appId 중 하나라도 없으면 웹에서 register()가 undefined를 돌려줍니다.env.client.ts가 AKAN_PUBLIC_ENV에 따라 env.client.<env>.ts를 고르므로, 배포하는 모든 환경에 채워 둡니다.firebase가 있으면 akan sync가 환경마다 public/firebase-messaging-sw.js를 써 줍니다.native.appId와 정확히 같은 Firebase Android 앱 등록, 그리고 native.android.googleServices가 지정하는 설정 파일 하나로 끝납니다.native.appId와 같은 패키지 이름을 입력합니다.google-services.json을 내려받습니다.apps/myapp/secrets/google-services.json에 둡니다.akan.config.ts의 native.android에서 지정합니다:appId인 client를 고르고(디버그 빌드도 그것을 씁니다), 그 앱이 없는 파일이면 들어 있는 이름을 알려 주며 빌드를 멈춥니다.public/이 아니라 secrets/에 둡니다. public/의 파일은 모든 방문자에게 그대로 제공됩니다. secrets에 등록한 파일은 git에서 빠지고, akan upload-env와 akan download-env로 함께 옮겨집니다.permissions: ["push"]가 앱에 푸시 플러그인과 POST_NOTIFICATIONS 권한을 넣습니다.

google-services.json은 서버 인증 정보가 아닙니다. Android 앱용 Firebase 설정 파일이지, Firebase Admin 서비스 계정 JSON이 아닙니다. 서버 인증 정보는 마지막 섹션처럼 env.server.*에 둡니다.url로 이동합니다.native.android.push로 channel({ id, name, importance? }), smallIcon(앱 폴더 안의 흰색·투명 PNG), 강조 color를 대신 정합니다.native.appId와 같은 App ID를 골라 Push Notifications를 켭니다..p8을 내려받습니다. 한 번만 받을 수 있으니, Key ID와 Team ID도 함께 적어 둡니다.pushNoti.apns에 넣습니다. 마지막 섹션에서 봅니다.native에 permissions: ["push"]를 추가합니다.GoogleService-Info.plist도, firebase-ios-sdk도 필요 없습니다. UIBackgroundModes와 aps-environment는 푸시 플러그인이 앱에 직접 넣습니다.provider: "apns"로 오며, FCM은 이 토큰을 받지 않으므로 서버가 직접 APNs로 보냅니다.xcrun simctl push에는 서버가 필요 없습니다. 시뮬레이터에 payload를 바로 넘겨 탭과 라우팅을 시험합니다. 서버처럼 url은 aps 옆, 최상위에 둡니다.

aps-environment는 직접 쓰지 않습니다. 푸시 플러그인이 development를 선언하고, 프로비저닝 프로파일로 서명한 빌드는 프로파일의 값을 씁니다. 이 값이 기기 토큰이 어느 APNs 환경의 것인지를 정합니다.| 명령 | aps-environment | 용도 |
|---|---|---|
| akan start-ios | development | 시뮬레이터와 development로 서명한 iPhone 실행이며, APNs 샌드박스를 씁니다. |
| akan build-ios | development | 시뮬레이터 빌드입니다. |
| akan release-ios | production | App Store 프로파일입니다. TestFlight와 App Store에 씁니다. |
| akan release-ios --adHoc | production | ad hoc 프로파일입니다. |
environment를 비워 두면 production에 먼저 보내고, APNs가 BadDeviceToken으로 답하면(development 빌드의 토큰) 샌드박스로 보냅니다. 하나로 고정하려면 environment를 적습니다.410이나, 마지막으로 시도한 환경의 BadDeviceToken이면 그 토큰을 주인에게서 지웁니다.libs/shared를 쓰는 앱은 직접 짤 코드가 없습니다. 로그인한 사용자의 레이아웃에 Notification.Zone.Initialize를 한 번 둡니다. 방문할 때마다, 그리고 네이티브 셸이 토큰을 바꿀 때마다 기기를 다시 등록하며, 권한은 묻지 않습니다.Notification.Util.PushSetting이 그 스위치입니다. 직접 만든 버튼이라면 register()를 부르고 받은 PushToken을 스토어에 넘깁니다:

registerPushToken은 libs/shared에 들어 있습니다. 쓰지 않는다면 PushToken을 직접 만든 엔드포인트에 넘기면 됩니다. 필드는 다음 섹션의 DeviceToken과 하나씩 맞습니다.@libs/util/webkit에서 가져옵니다. 대부분의 화면은 register()만 있으면 됩니다.PushToken을 돌려줍니다. 거부되거나 지원하지 않으면 undefined입니다.getPermission()을 확인합니다.PushToken을 리스너에 넘기고, 해제 함수를 돌려줍니다.token, platform(web | android | ios), provider(apns | fcm), 그리고 getPushDeviceId()가 앱 저장소에 두는 설치 id인 deviceId가 들어 있습니다.libs/shared를 쓰면 st.do.registerPushToken(pushToken)이 로그인한 사용자에게 저장합니다. 어디에 두는지는 다음 섹션에서 봅니다.url을 보내면 탭했을 때 CSR router로 그 경로를 엽니다. 네이티브 셸에서는 앱을 띄운 탭까지 포함해 프레임워크가 부팅 때부터 라우팅하고, 브라우저에서는 서비스 워커가 열린 탭에 넘깁니다. 앱 안의 경로만 따라갑니다.libs/shared는 기기마다의 토큰을 그 주인에게 둡니다. user.notiInfo.deviceTokens에 설치 하나당 DeviceToken 하나씩입니다. secret 필드라 서버 밖으로 나가지 않습니다.register()가 돌려준 값과, 서버가 저장한 시각을 담습니다:token이나 같은 deviceId로 다시 등록하면 그 항목을 바꾸므로, 바뀐 토큰이 쌓이지 않습니다.updatedAt은 서버가 씁니다. 토큰을 등록할 때 기록하며, 클라이언트가 보낸 값은 쓰지 않습니다.signoutUser가 설치의 deviceId를 보내므로, 물려받은 폰이 앞 사람의 알림을 받지 않습니다.Notification.Zone.Initialize가 다음 방문 때 기기를 다시 등록합니다.user 시그널에 있는 User 가드 엔드포인트이며, notification 스토어의 registerPushToken, unregisterPushToken, loadPushState가 부릅니다:DeviceToken을 호출한 사용자에게 저장하고, 이전 항목을 바꿉니다.Self가 넘겨줍니다. 그래서 클라이언트가 다른 사람 계정으로 토큰을 등록할 수 없습니다.mcp: false입니다.notificationService.push(userIds, payload) 하나입니다. 받는 사람마다 수신 설정을 읽고, 받아 준 기기마다 그 기기의 provider로 보내며, APNs나 FCM이 사라졌다고 답한 토큰을 지웁니다.firebase는 서비스 계정입니다. Firebase Console의 프로젝트 설정 → 서비스 계정에서 받고, 내려받은 JSON에서 위 다섯 필드를 옮겨 적습니다. Android와 웹에 필요합니다.apns는 .p8 키입니다. privateKey는 파일의 텍스트이고(\n 이스케이프도 됩니다), keyId와 teamId는 Apple Developer에서, bundleId는 앱의 native.appId입니다. iOS에 필요합니다.google-services.json이 아닙니다. 그 파일은 Android 앱 설정이고, 이것들은 모든 발송에 서명합니다.

NotificationService.accepts입니다. block과 disagree는 전부 막고, fewer는 actionRequired와 essential만 통과시키며, pauseUntil이 미래면 전부 막고, 토큰이 없는 사용자는 건너뜁니다.410이나 BadDeviceToken, FCM의 messaging/registration-token-not-registered를 받으면 같은 호출 안에서 주인에게서 지웁니다.push()는 닿은 범위(targetUserIds, tokenNum, successCount, prunedTokens)를 돌려주고, 발송 실패가 호출한 쪽의 일을 실패시키지 않습니다.type: "all" 알림은 모든 활성 사용자에게 500명씩 나가며, 다른 푸시처럼 accepts를 거칩니다.actionRequired, notice, essential, suggestion, advertise 중 하나입니다. 수신 설정 판정이 읽습니다..p8은 서버에만 둡니다. 이 키는 팀의 모든 앱에 푸시를 서명합니다. env.server.*에 두고, env.client.*나 public/에는 절대 두지 않습니다.pushNoti.apns is not configured 같은 warn 로그 한 줄만 남기고 그 토큰들을 건너뛰며, 실패로 셉니다.libs/shared 없이 쓴다면 @libs/util/srvkit의 PushNotificationServer.sendEach(targets, message)를 { token, provider } 목록으로 부르고, 돌려받은 invalidTokens는 더 이상 저장하지 않습니다.