Skip to content

현재 사용법 (2026-09-10, 6fbaee7f): 알림 조회·설정·목적지 이동 · 기능 마스터. 아래는 작성 당시의 변경 설명이며 현재 전체 동작·재측정 결과를 뜻하지 않습니다.

알림을 눌렀을 때 이동하거나, 어디서 보는지 알려줍니다 ​

알림 목록에서 항목을 눌러도 아무 일이 없던 것을, 해당 화면으로 이동하거나(휴가 승인 알림 → 결재 > 휴가신청) 그 알림을 볼 수 있는 앱을 알려주는(급여 시트 셀 메모 알림 → 「급여관리에서 확인해 주세요.」) 동작으로 바꿉니다.

세 갈래(직원 앱 알림 목록·데스크톱 알림 벨·푸시 탭)의 근본 원인은 동일합니다. 「이 알림은 어디로 가는가」를 정하는 표가 한 곳에 없었습니다. 목록은 아예 판단하지 않았고, 벨은 자기 표를 들고 있었으나 그 판단이 화면에 닿지 못했으며, 푸시는 서버가 일부 경로에서만 목적지를 실어 보냈습니다. 표를 하나로 모으자 세 갈래가 함께 해결됩니다.

배경 / 지금 동작 ​

직원 앱(employee app, 포트 4002, 모바일)의 상단 헤더에 있는 종 아이콘을 누르면 알림 목록 화면이 열립니다. 이 화면의 마크업은 apps/employee-app/src/views/app-shell.html에 있고, 상태와 클릭 처리는 앱 셸 컴포넌트(apps/employee-app/src/app.ts)가 갖습니다.

알림 한 건은 서버의 NotificationEvent 행이며, 인박스 응답은 제목·본문과 함께 type(알림 종류. leave_request·memo 등 13종의 DB enum), referenceType(그 알림이 가리키는 원본 종류. sheet_memo·grade_promotion 등), metadata를 내려줍니다.

지금까지 목록 항목의 클릭 처리(readNotification)는 읽음 표시만 했습니다. 코드 주석에 「상세 화면은 아직 없어 읽음 처리만 한다」고 적혀 있고, 사양서 HBEA110도 이 이동을 「FIG 정책상 후순위」로 기록해 두었습니다. 서버는 이미 referenceType과 metadata를 실어 보내고 있었으므로, 없던 것은 데이터가 아니라 라우팅입니다.

데스크톱 앱(관리자 등)에는 별도의 공용 알림 벨(packages/components/src/components/NotificationBell.ts) 드롭다운이 있습니다. 여기에는 타입→화면 매핑이 있었지만, 이동을 navigate-section(이동 대상을 담아 window에 쏘는 커스텀 이벤트)으로 알리는 구조였습니다. 그 이벤트를 듣는 앱은 직원 앱뿐이고, 직원 앱은 이 드롭다운 대신 위의 전용 알림 화면을 씁니다. 즉 아무도 받지 않는 이벤트였습니다. 벨의 매핑에는 DB enum에 존재하지 않는 키(message·conversation)까지 남아 있었습니다.

푸시 알림(잠금화면 알림)을 탭했을 때의 이동은 서버가 payload에 section 필드를 넣어 줄 때만 성립합니다. 넣는 곳은 메신저 3곳과 결재 1곳뿐이었고, 일반 알림 공통 경로(notification.service.ts)는 type·action·referenceType만 담았습니다.

이 문서에서 쓰는 두 용어를 구분합니다. 목적지는 직원 앱 안의 화면(하단 탭과 결재의 하위 탭)이고, 홈 앱은 그 알림이 원래 다뤄지는 앱 이름(「급여관리」·「관리자」·「내부 결재」)입니다. 앱 이름은 워크스페이스 전환기(WorkspaceSwitcher)의 표기를 그대로 씁니다.

판단 경로 ​

변경 전

변경 후

개념 흐름별 변경 ​

목적지 표를 @hereby/core에 하나만 둡니다. packages/core/src/constants/notification-navigation.ts에 resolveNotificationTarget을 신설하고, 알림 목록·알림 벨·서버 푸시가 모두 이 함수를 부릅니다. 목적지는 type 하나로 결정됩니다 — referenceType은 같은 타입 안에서 더 잘게 나뉠 뿐 목적지가 같기 때문입니다(예: overtime의 overtime과 overtime_record는 둘 다 결재 > 근무). 표가 흩어져 있으면 새 알림 타입이 생겼을 때 한 곳만 고치고 나머지는 조용히 예전 화면으로 보냅니다. 벨의 죽은 키(message·conversation)는 이때 정리됩니다.

알림 목록이 실제로 이동합니다. readNotification이 목적지를 받아 섹션을 전환합니다. 결재 탭은 섹션을 바꾸기 전에 지정합니다 — 순서를 뒤집으면 이전 탭이 한 프레임 그려졌다 바뀝니다. 이동을 읽음 처리보다 먼저 하는 이유는 markRead가 서버 왕복이라 기다리면 탭 반응이 늦어지기 때문이며, 읽음 처리가 실패하면 읽음 표시만 되돌립니다.

직원 앱에 화면이 없는 알림은 어디서 보는지 알려줍니다. 급여 시트 셀 메모·권한 요청·호봉 승급처럼 모바일에 대응 화면이 없는 알림을 결재 화면으로 보내면, 관계없는 목록이 열려 오히려 고장으로 읽힙니다. 대신 resolveNotificationAppLabel이 돌려주는 홈 앱 이름으로 「{앱이름}에서 확인해 주세요.」 컨펌 다이얼로그(제목 「확인」, 확인 버튼 하나)를 띄우고 화면은 그대로 둡니다. system 타입만 호출자에 따라 앱이 갈리므로(호봉 승급은 급여관리, 근무형태 변경은 내부 결재) referenceType을 먼저 봅니다.

알림 벨이 이동 성사 여부를 알 수 있게 합니다. 이벤트는 듣는 쪽이 없으면 그냥 사라지므로, 벨은 이동이 됐는지 판단할 수 없었습니다. navigate-section을 cancelable로 쏘고, 처리한 쪽이 preventDefault()로 소비를 표시하게 했습니다(직원 앱 리스너는 실제로 이동에 성공했을 때만 표시합니다). 아무도 받지 않았으면 벨도 같은 안내 다이얼로그를 띄웁니다. 이를 위해 홈 앱 표는 모든 알림 타입을 덮습니다 — 직원 앱에 화면이 있는 승인 흐름 6종도 홈 앱(내부 결재)을 갖습니다.

푸시 탭도 같은 표를 씁니다. 서버의 공통 알림 경로가 resolveNotificationTarget으로 section·approvalTab·conversationId를 payload에 채웁니다. 앱이 켜져 있을 때는 서비스워커가 이 값을 창으로 전달하고, 앱이 꺼져 있을 때는 받을 창이 없어 ?section=… 쿼리로 새 창을 여는데 앱이 그 쿼리를 읽지 않고 있었습니다. 첫 렌더에서 한 번 읽고 주소창을 되돌립니다(남겨두면 새로고침마다 같은 화면으로 다시 이동합니다). 이동 대상 파싱은 @capacitor/*에 의존하지 않는 utils/push-navigation.ts로 분리했습니다 — 앱 셸이 초기 번들에서 써야 하기 때문입니다.

안드로이드 하드웨어 백이 다이얼로그를 먼저 닫습니다. 다이얼로그가 열린 상태에서 백을 누르면 그 아래 층이 백을 가져갔습니다. 홈 탭에서는 「앱을 종료하시겠습니까?」 확인이 열린 다이얼로그 위에 겹쳐 떴고(같은 컴포넌트가 내용만 갈아끼웁니다), 다른 탭에서는 다이얼로그를 띄운 채 홈으로 이동했습니다. dismissOpenDialog()를 신설해 백 핸들러가 app-back-button을 쏘기 전에 열린 다이얼로그부터 닫습니다. 취소 값은 Esc를 눌렀을 때와 같습니다(confirm은 false, prompt는 null, choice는 "cancel"). 알림 안내뿐 아니라 기존 askConfirm 사용처 전부에 적용됩니다.

새 알림 타입이 조용히 빠지지 않게 합니다. NotificationType 전체를 돌며 모든 타입이 홈 앱을 갖는지 검사하는 단위 테스트를 두었습니다. 새 타입을 추가하면 이 테스트가 먼저 실패하므로, 눌러도 아무 일이 없는 알림이 늘어나지 않습니다.

사양서를 갱신합니다. HBEA110에서 「후순위·라이브 미구현 후보」로 남아 있던 행을 구현 내용으로 바꾸고, 유형별 목적지(ㄴ33-2)와 이동 없는 유형의 안내 앱(ㄴ33-3) 행을 새로 두었습니다.

검증 ​

localhost:4002에서 알림을 유형별로 심어 클릭했습니다. leave_request는 결재 > 휴가신청, overtime은 결재 > 근무, certificate는 결재 > 서류관리로 이동했고, permission_request·memo·system은 각각 「관리자」·「급여관리」 안내 다이얼로그가 떴습니다.

알림 벨의 분기(듣는 앱이 있을 때와 없을 때)는 화면으로 재현하기 어려우므로 단위 테스트로 덮었습니다. 실행 중인 admin dev 서버는 @hereby/components를 미리 번들해 캐시하므로, 벨 변경을 화면으로 확인하려면 dev 서버 재기동이 필요합니다.