feat: 알림 항목을 누르면 해당 게시판으로 간다
알림에 대상 컬럼이 없어(TB_COM_NTCN: 제목·내용·읽음뿐) 제목으로 가른다 — 지금 쌓이는
알림은 학습자 1:1문의 등록 하나라 1:1문의 게시판(/boards/inquiries)으로 보낸다. 누르면
드롭다운을 닫고, 링크는 hover·focus 표시를 갖는다. 모르는 알림은 종전처럼 누를 수 없다.
내용 줄은 두 줄로 잘라 요약처럼 보이게 하고 전체는 title로 남긴다. 다만 백엔드가
내용을 고정 문구("학습자가 1:1 문의 게시판에 게시글을 등록하였습니다")로 넣고 있어
실제 게시글 요약은 CmmBbsServiceImpl.insert가 ntcnCn에 글 본문을 실어 줘야 나온다.
Co-Authored-By: Claude Opus 5
@550f9a4bd3a255d7099b60cf6f668e6dc56e434f
--- app/(protected)/(basic)/_components/admin-header.module.scss
+++ app/(protected)/(basic)/_components/admin-header.module.scss
... | ... | @@ -123,13 +123,36 @@ |
| 123 | 123 |
letter-spacing: -0.025em; |
| 124 | 124 |
} |
| 125 | 125 |
|
| 126 |
+.notificationLink {
|
|
| 127 |
+ color: inherit; |
|
| 128 |
+ text-decoration: none; |
|
| 129 |
+ cursor: pointer; |
|
| 130 |
+ |
|
| 131 |
+ &:hover {
|
|
| 132 |
+ background: fox.color(action-default-hover); |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ &.notificationUnread:hover {
|
|
| 136 |
+ background: fox.color(action-primary-pressed); |
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ &:focus-visible {
|
|
| 140 |
+ outline: fox.border(2) solid fox.color(border-theme-primary); |
|
| 141 |
+ outline-offset: calc(-1 * #{fox.border(2)});
|
|
| 142 |
+ } |
|
| 143 |
+} |
|
| 144 |
+ |
|
| 126 | 145 |
.notificationContent {
|
| 146 |
+ display: -webkit-box; |
|
| 147 |
+ overflow: hidden; |
|
| 127 | 148 |
color: fox.color(font-neutral-subtle); |
| 128 | 149 |
font-family: fox.font-family(body); |
| 129 | 150 |
font-size: fox.font-size(body-sm); |
| 130 | 151 |
line-height: 1.5; |
| 131 | 152 |
letter-spacing: -0.025em; |
| 132 | 153 |
word-break: break-word; |
| 154 |
+ -webkit-box-orient: vertical; |
|
| 155 |
+ -webkit-line-clamp: 2; |
|
| 133 | 156 |
} |
| 134 | 157 |
|
| 135 | 158 |
.notificationEmpty {
|
--- app/(protected)/(basic)/_components/admin-header.tsx
+++ app/(protected)/(basic)/_components/admin-header.tsx
... | ... | @@ -1,9 +1,14 @@ |
| 1 | 1 |
'use client'; |
| 2 | 2 |
|
| 3 | 3 |
import Image from 'next/image'; |
| 4 |
+import Link from 'next/link'; |
|
| 4 | 5 |
import { useEffect, useRef, useState } from 'react';
|
| 5 | 6 |
import { logout } from '@/app/(protected)/_actions';
|
| 6 |
-import { countUnread, type AdminNotification } from '@/lib/domain/notification';
|
|
| 7 |
+import {
|
|
| 8 |
+ countUnread, |
|
| 9 |
+ resolveNotificationHref, |
|
| 10 |
+ type AdminNotification, |
|
| 11 |
+} from '@/lib/domain/notification'; |
|
| 7 | 12 |
import styles from './admin-header.module.scss'; |
| 8 | 13 |
import { HeaderIconButton } from './header-icon-button';
|
| 9 | 14 |
|
... | ... | @@ -107,22 +112,38 @@ |
| 107 | 112 |
<p className={styles.notificationEmpty}>새 알림이 없습니다.</p>
|
| 108 | 113 |
) : ( |
| 109 | 114 |
<ul className={styles.notificationList}>
|
| 110 |
- {notifications.map((item) => (
|
|
| 111 |
- <li key={item.id}>
|
|
| 112 |
- <div |
|
| 113 |
- className={`${styles.notification} ${
|
|
| 114 |
- item.isRead ? '' : styles.notificationUnread |
|
| 115 |
- }`} |
|
| 116 |
- > |
|
| 115 |
+ {notifications.map((item) => {
|
|
| 116 |
+ const href = resolveNotificationHref(item); |
|
| 117 |
+ const className = `${styles.notification} ${
|
|
| 118 |
+ item.isRead ? '' : styles.notificationUnread |
|
| 119 |
+ }`; |
|
| 120 |
+ const body = ( |
|
| 121 |
+ <> |
|
| 117 | 122 |
<span className={styles.notificationTitle}>
|
| 118 | 123 |
{item.title}
|
| 119 | 124 |
</span> |
| 120 |
- <span className={styles.notificationContent}>
|
|
| 125 |
+ <span className={styles.notificationContent} title={item.content}>
|
|
| 121 | 126 |
{item.content}
|
| 122 | 127 |
</span> |
| 123 |
- </div> |
|
| 124 |
- </li> |
|
| 125 |
- ))} |
|
| 128 |
+ </> |
|
| 129 |
+ ); |
|
| 130 |
+ return ( |
|
| 131 |
+ <li key={item.id}>
|
|
| 132 |
+ {href ? (
|
|
| 133 |
+ <Link |
|
| 134 |
+ href={href}
|
|
| 135 |
+ role="menuitem" |
|
| 136 |
+ className={`${className} ${styles.notificationLink}`}
|
|
| 137 |
+ onClick={() => setIsNotificationOpen(false)}
|
|
| 138 |
+ > |
|
| 139 |
+ {body}
|
|
| 140 |
+ </Link> |
|
| 141 |
+ ) : ( |
|
| 142 |
+ <div className={className}>{body}</div>
|
|
| 143 |
+ )} |
|
| 144 |
+ </li> |
|
| 145 |
+ ); |
|
| 146 |
+ })} |
|
| 126 | 147 |
</ul> |
| 127 | 148 |
)} |
| 128 | 149 |
</div> |
--- lib/domain/notification.ts
+++ lib/domain/notification.ts
... | ... | @@ -1,3 +1,5 @@ |
| 1 |
+import { BOARD_PATHS } from '@/lib/domain/board-post-query';
|
|
| 2 |
+ |
|
| 1 | 3 |
/** |
| 2 | 4 |
* 관리자 알림 한 건. 백엔드 `CmmNtcnVo`(`TB_COM_NTCN`). |
| 3 | 5 |
* |
... | ... | @@ -20,3 +22,14 @@ |
| 20 | 22 |
): number {
|
| 21 | 23 |
return notifications.filter((item) => !item.isRead).length; |
| 22 | 24 |
} |
| 25 |
+ |
|
| 26 |
+/** |
|
| 27 |
+ * 이슈: `TB_COM_NTCN`에 대상(게시판·게시글) 컬럼이 없어 제목으로 가른다. 지금 적재되는 알림은 |
|
| 28 |
+ * 학습자 1:1문의 등록(`CmmBbsServiceImpl.insert`, 제목 "학습자 1:1 문의 등록") 하나다. |
|
| 29 |
+ * 모르는 알림은 null — 누를 곳이 없다. |
|
| 30 |
+ */ |
|
| 31 |
+export function resolveNotificationHref( |
|
| 32 |
+ notification: AdminNotification |
|
| 33 |
+): string | null {
|
|
| 34 |
+ return notification.title.includes('1:1 문의') ? BOARD_PATHS.inquiry : null;
|
|
| 35 |
+} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?