chore: 앱 서버 브랜치(14단계 · 문구 다듬기 · main 을 합친 판)를 앱 첫 판 브랜치에 합친다
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 22m26s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 13m39s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 13m20s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m2s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m12s
CI (Flutter) / Flutter (analyze · test) (push) Successful in 22m15s
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 13m53s
CI / Frontend (lint · type-check · test · build) (push) Successful in 13m13s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 1m56s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m48s

이 PR 이 앱 서버 PR 뒤에 빨리 감기로 합쳐지고 CI 가 합친 모습 그대로를 시험하게 한다.
계약 1절의 앱 절은 앱 서버 쪽이 들고 있고 이 브랜치는 2절 줄만 더했다 — 충돌 없음.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #21.
This commit is contained in:
2026-11-08 12:39:17 +09:00
222 changed files with 11922 additions and 925 deletions
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
import { generateKeyPairSync, type KeyObject } from 'crypto';
/**
* 시험용 FCM 서비스 계정 키 — 프로세스마다 RSA 2048 을 한 번 만들어 쓴다(진짜 키가 아니다 — Google 에 보내지 않는다).
* 단위 시험(fcm-*.spec.ts · env.validation.spec.ts)이 env 값(base64 한 줄) · 서명 확인(공개 키)에 쓴다
*/
export interface TestServiceAccount {
/** 키 파일 JSON — 칸을 바꿔 틀린 키를 만든다 */
readonly json: Record<string, unknown>;
/** env 에 넣는 값 — JSON 을 통째로 base64 한 줄 */
readonly base64: string;
readonly publicKey: KeyObject;
readonly privateKeyPem: string;
}
let pair: { publicKey: KeyObject; privateKeyPem: string } | null = null;
function keyPair(): { publicKey: KeyObject; privateKeyPem: string } {
if (!pair) {
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
});
pair = {
publicKey,
privateKeyPem: privateKey
.export({ type: 'pkcs8', format: 'pem' })
.toString(),
};
}
return pair;
}
/** JSON → env 값 (base64 한 줄) */
export function toEnvValue(json: unknown): string {
return Buffer.from(JSON.stringify(json)).toString('base64');
}
/** 시험용 키 — `overrides` 로 칸을 바꾸거나(undefined 면 그 칸을 뺀다) 더한다 */
export function testServiceAccount(
overrides: Record<string, unknown> = {},
): TestServiceAccount {
const { publicKey, privateKeyPem } = keyPair();
const json: Record<string, unknown> = {
type: 'service_account',
project_id: 'arasystem-push-test',
private_key_id: '0123456789abcdef0123456789abcdef01234567',
private_key: privateKeyPem,
client_email: 'arasystem-push@arasystem-push-test.iam.gserviceaccount.com',
client_id: '100000000000000000000',
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
token_uri: 'https://oauth2.googleapis.com/token',
...overrides,
};
for (const [key, value] of Object.entries(overrides)) {
if (value === undefined) delete json[key];
}
return { json, base64: toEnvValue(json), publicKey, privateKeyPem };
}