generated from ttipo/project-base
feat: 앱 기반과 앱 서버 — 앱 전용 로그인 · 새 메일 알림(FCM) · 앱 설정 #20
Reference in New Issue
Block a user
Delete Branch "feat/app-push-server"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
무엇을 · 왜
모바일 앱(Flutter — 안드로이드 먼저, 첫 판 = 로그인 · 메일 · 새 메일 알림)을 위한 서버와 앱 기반이다. 11-05 사용자 결정(웹 · 운영과 나란히 앱 개발)에 따라 웹과 같은 세션 규칙을 앱 토큰 경로로 쓴다.
POST /api/auth/app/login·refresh·logout+ 보호 API 는Authorization: Bearer. 웹과 같은AuthService(실패 카운터 · 2단계 코드 가드 · 앱 비밀번호 거절 · 막는 계정)를 지나고, 토큰은 응답 본문(no-store), 세션 표에client(web · app — 마이그레이션AddAppSessionClient, DB 기본값 web). 앱 세션의 30초 유예는 그 세션을 끝낸다.PUT·DELETE /api/push-registrations/current·DELETE …/current/unseen(앱 토큰만), 표push_registrations(마이그레이션AddPushRegistrations). 새 메일 신호 → 같은 직원 30초에 한 번 → "새 메일 N통"만(내용 · 주소 · 보낸 사람 없음), 저절로 끝난 세션 · 다른 곳 비밀번호 바꿈 등에는 "알림이 멈췄습니다" 한 번. 새 의존성 없음(node crypto RS256 JWT · fetch). 키FCM_SERVICE_ACCOUNT_KEY(backend env — 선택, 없으면 알림만 끄고 뜬다 · 기동 로그로 확인).GET /api/app-config(로그인 없음) — 앱 최소 판 ·pushAvailable.kr.it.arasystem.app, Flutter 3.41.9 고정(CI 와 같게), 로그인 · 토큰 갱신 · 모듈 라우터 · 인터셉터.f0ef782· 이번 합침). 이번 합침의 충돌 넷은 모두 로그인된 기기 글이었다 — 뜻은 앱 쪽("브라우저" → "기기"), 문장은 다듬은 글("이 목록에 없습니다")로 풀었다.확인 방법
backend/.env.production에, backend →oauth2.googleapis.com·fcm.googleapis.com443 · 서버 시계(NTP) — docker 스킬 "백엔드의 바깥 연결".영향 범위
체크리스트
BusinessException/error.code분기)npm run sync:error-codes실행 후 생성 파일까지 커밋 (바꾸지 않음)*.example값은 placeholder — FCM 키 · google-services.json 은 커밋하지 않음)🤖 Generated with Claude Code
14단계 3차다. 같은 대화(답장 · 전달로 이어진 메일)를 읽기 화면에 보이고, 목록을 대화로 묶어 볼 수 있게 한다. 색인이 준비된 직원은 우리 대화 키로, 채우는 중인 직원은 엔진의 스레드로 묶는다(응답의 basis — 첫 쪽에서 정하고 커서가 잇는다). - 대화 키: 색인 쓰기 때 넣은 메일의 부모 · 자식(References · In-Reply-To) 가운데 말머리 (회신 · 답장 · 전달 포함)를 뗀 제목이 같은 것을 가장 작은 키로 합친다. 늦게 온 원본도 잇고, 다시 넣어도 키는 풀리지 않는다. 색인 판 4 - GET /api/messages/{id}/conversation — 오래된 차례, 휴지통 · 스팸메일함에만 든 메일과 발송 중인 내부 초안은 빼고 지금 메일은 늘 넣는다 - GET /api/conversations?mailboxId= — 그 메일함의 대화 한 줄씩(참여자 10명 · 통 수 · messageIds 최신 500 · pendingSendIds). 직원 주소 btree 로 먼저 좁힌다(10명 흉내 큰 메일함 첫 쪽 81ms) - 설정 groupByConversation(처음 끔) — 서버는 저장만 하고 묶을지는 화면이 정한다 - 마이그레이션 AddMailSearchSender — 참여자는 따로 둔 첫 보낸 사람 칸에서만 읽는다 리뷰 반영: 엔진 방식에 개수 · 시간 상한(같은 대화 한 요청 1,000통 · 묶은 목록 스레드마다 500 · 한 요청 2,000 · 15초, 요약 속성은 내보낼 메일만), Mailbox/get 장애는 503, 꾸민 커서는 엔진 · DB 보다 먼저 400, 같은 대화 한 문장, 이름 안의 <주소>로 참여자를 꾸미지 못하게, 로그 추적 ID. 검색 표본 표 머리 주석(frontend 로 옮겼다)도 함께 고친다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>- 분할 보기에서 대화 줄을 고르면 도구 줄이 두 줄로 넘치던 것, 목록 머리의 안 읽은 수가 1440 에서도 늘 잘리던 것 - 묶은 줄에 한 일의 알림은 "{메일함}에 있는 메일 N통을 …"(줄의 통 수는 대화 전체라서), 되돌리기 알림은 되돌린 메일함을 말한다 - 같은 대화 칸: 펼친 줄의 첨부 수 안내, 우리 원문 인용 머리부터 뺀 미리 보기, 보내기를 기다리는 메일은 줄 끝에 - 검색 준비 중 · 결과 없음 · 하루짜리 기간 · 내 이름 조건의 글, 예약 시각의 띄어쓰기, 이름 맞추기(대화로 묶기 · 내용에 든 말 · 표 머리 참여자) - 다른 탭 · PC 에서 바꾼 설정을 탭으로 돌아올 때 다시 받는다(60초 지났을 때) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>- 대화로 묶은 휴지통에서 줄을 골라 영구 삭제하면 확인 창 · 알림을 "휴지통에 있는 메일 N통을 …"으로 쓴다 — 줄의 통 수는 대화 전체라서 (옮기기 알림 "{메일함}에 있는 메일 N통을 …"과 같은 결) - 값을 끼워 넣는 글의 띄어 쓴 조사를 붙인다(9단계 "@도메인에서", 11단계 "25MB를" · "2GB까지" · "20MB가"), 공개 내려받기 화면의 신고 주소는 끝 글자로 조사를 고른다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>계약 1절 끝의 옛 메모(/api/push-devices · { minVersion, latestVersion, storeUrl })를 확정한 경로 · 모양 · 규칙으로 바꾸고 (N · 간격 · 따라잡기 · 엔진 after 의 초 경계 · "알림이 멈췄습니다"를 보내는 까닭 표 · 실패 처리 · 실기기로 확인할 것), "클라이언트 — 앱"에 앱이 실제로 부르는 때를 적었다. 2절 실시간 알림의 붙잡기와 6절 퇴사 마무리 목록도 더했다. nestjs · docker 스킬과 env · 보안 · 로그 규칙에 FCM 키 자리 · 바깥 연결 · 가릴 값을 더했다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>이 PR 이 문구 다듬기 PR 뒤에 빨리 감기로 합쳐지고 CI 가 합친 모습 그대로를 시험하게 한다. 충돌 넷은 모두 설정 › 로그인된 기기의 글이다 — 앱 쪽은 앱 세션이 함께 보여 "브라우저"를 "기기"로, 다듬기 쪽은 같은 안내 문장을 규칙 9 로("여기에 보이지 않습니다" → "이 목록에 없습니다") 바꿨다. 뜻은 앱 쪽 · 문장은 다듬은 글로 풀었고(account-security.ts · LoginSessionList.vue 주석 · 디자인 README), 시험은 두 쪽이 더한 상수를 모두 남기고 글자 그대로 견주던 줄을 상수로 바꿨다(AccountLoginHistoryPage.spec.ts). 합친 뒤 backend tsc · lint 0 · 단위 3,701 · e2e 333, frontend type-check · lint 0 · 단위 2,774 통과. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>