Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1e91719
Merge pull request #113 from codeit-FE18-part3/main
sohyun0 Oct 20, 2025
4008b5e
πŸ›fix: approved -> accepted μˆ˜μ • #99
gummmmmy0v0 Oct 20, 2025
c1177ad
Merge pull request #114 from gummmmmy0v0/feature/#99
sohyun0 Oct 20, 2025
caa50c6
πŸ›fix: 곡고 등둝 λ‚ μ§œ 였λ₯˜ #100
gummmmmy0v0 Oct 20, 2025
ea84793
πŸ›fix: λ‚ μ§œ #101
gummmmmy0v0 Oct 20, 2025
db82526
Merge pull request #115 from gummmmmy0v0/feature/#101
sohyun0 Oct 20, 2025
2df1437
♻️ Refact: ν† μŠ€νŠΈ λ©”μ‹œμ§€ νŽ˜μ΄μ§€ μ΄λ™μ‹œ null 적용
BaeZzi813 Oct 20, 2025
68d3b7b
Merge pull request #116 from BaeZzi813/jaeyeong2
sohyun0 Oct 20, 2025
8f1ff88
✨ feat: μ•Œλ¦Ό 읽음 처리 API μ—°κ²°, notificationOn ν™œμ„±ν™”
jeschun Oct 20, 2025
fe29f78
πŸ’„ style: μ•Œλ¦Ό λ²„νŠΌ 쀑볡 원 제거
jeschun Oct 20, 2025
010eaa7
πŸ› fix: μ§μ›μ—κ²Œλ§Œ μ•„μ΄μ½˜
jeschun Oct 20, 2025
e014d96
πŸ› fix: 사μž₯λ‹˜ μ•Œλ¦Όλ²„νŠΌ μ•ˆλ³΄μ΄κ²Œ
jeschun Oct 20, 2025
c1a58ef
Merge pull request #117 from jeschun/ShinCheon
sohyun0 Oct 20, 2025
2c9c2fc
✨ feat: μ •λ ¬ λ³€κ²½ μ‹œ νŽ˜μ΄μ§€λ„€μ΄μ…˜ 1번으둜 이동 #97
sohyun0 Oct 20, 2025
6eba7df
πŸ’„ design: λ ˆμ΄μ•„μ›ƒ μ‹œν”„νŠΈ λ°©μ§€ #97
sohyun0 Oct 20, 2025
a073546
Merge pull request #118 from sohyun0/feat/#97-modification
sohyun0 Oct 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions src/components/features/noticeList/noticeListSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const NoticeListSection = ({ q, initialFilters }: NoticeListSectionProps) => {
<NoticeListHeader q={q} />
<NoticeListFilter
filters={filters}
onSortChange={sort => fetchNotices({ sort })}
onFilterSubmit={filter => fetchNotices(filter)}
onSortChange={sort => fetchNotices({ sort, offset: 0 })}
onFilterSubmit={filter => fetchNotices({ ...filter, offset: 0 })}
/>
</div>
<NoticeList
Expand All @@ -40,15 +40,16 @@ const NoticeListSection = ({ q, initialFilters }: NoticeListSectionProps) => {
isInitialized={isInitialized}
reset={reset}
/>
{!isLoading && (
<Pagination
total={pagination.count}
limit={pagination.limit}
offset={pagination.offset}
onPageChange={next => fetchNotices({ offset: next })}
className='mt-8 tablet:mt-10'
/>
)}
<div className='mt-8 tablet:mt-10 min-h-[40px]'>
{!isLoading && (
<Pagination
total={pagination.count}
limit={pagination.limit}
offset={pagination.offset}
onPageChange={next => fetchNotices({ offset: next })}
/>
)}
</div>
</Container>
);
};
Expand Down
88 changes: 63 additions & 25 deletions src/components/layout/header/nav.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// src/components/layout/header/nav.tsx (μ˜ˆμ‹œ 경둜)
import { getUserAlerts, markAlertRead } from '@/api/alerts';
import { Icon } from '@/components/ui';
import Notification, { type Alert } from '@/components/ui/modal/notification/Notification';
import { useUserApplications } from '@/context/userApplicationsProvider';
import useAuth from '@/hooks/useAuth';
import { cn } from '@/lib/utils/cn';
import { UserRole } from '@/types/user';
import Link from 'next/link';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';

interface NavItems {
href: string;
Expand All @@ -22,44 +24,79 @@ const NAV_ITEMS: Record<UserRole, NavItems[]> = {
};

const Nav = () => {
const { role, isLogin, logout } = useAuth();
const { role, isLogin, logout, user } = useAuth();
const { applications } = useUserApplications();

const [open, setOpen] = useState(false);
// 읽음 μ²˜λ¦¬ν•œ μ•Œλ¦Ό IDλ“€ (간단 둜컬 μƒνƒœ)
const [readIds, setReadIds] = useState<Set<string>>(new Set());
const [apiAlerts, setApiAlerts] = useState<Alert[]>([]);

// μ•Œλ°”λ‹˜ μ•Œλ¦Ό: 승인/거절만 ν‘œμ‹œ
const alerts: Alert[] = useMemo(() => {
// 1) μ„œλ²„ μ•Œλ¦Ό 뢈러였기 (사μž₯λ‹˜/μ•Œλ°” 곡톡)
useEffect(() => {
if (!isLogin || !user?.id) {
setApiAlerts([]);
return;
}
(async () => {
try {
const res = await getUserAlerts(user.id, { offset: 0, limit: 50 });
const mapped: Alert[] = (res.items ?? []).map(({ item }) => ({
id: item.id,
createdAt: item.createdAt,
result: item.result,
read: item.read,
shop: { item: item.shop.item },
notice: { item: item.notice.item },
}));
setApiAlerts(mapped);
} catch {
setApiAlerts([]); // μ‹€νŒ¨ν•΄λ„ UIλŠ” λ™μž‘(직원 fallback)
}
})();
}, [isLogin, user?.id]);

// 2) (직원 μ „μš©) 지원내역 기반 fallback μ•Œλ¦Ό
const fallbackAlertsForEmployee: Alert[] = useMemo(() => {
if (role !== 'employee') return [];
return applications
.filter(a => a.item.status !== 'pending')
.map(a => ({
id: a.item.id,
createdAt: a.item.createdAt ?? new Date().toISOString(),
result: a.item.status === 'accepted' ? 'accepted' : 'rejected',
// β–Ά 읽음: μ‚¬μš©μžκ°€ λ©”μ‹œμ§€λ₯Ό ν΄λ¦­ν–ˆμ„ λ•Œλ§Œ true
read: readIds.has(a.item.id),
shop: { item: a.item.shop.item, href: `/shops/${a.item.shop.item.id}` },
notice: { item: a.item.notice.item, href: `/notices/${a.item.notice.item.id}` },
}));
}, [applications, readIds]);
}, [applications, role, readIds]);

// 3) μ‹€μ œ ν‘œμ‹œν•  μ•Œλ¦Ό: μ„œλ²„ κ²°κ³Όκ°€ 있으면 μš°μ„ , μ—†μœΌλ©΄(특히 직원) fallback
const alerts: Alert[] = useMemo(() => {
const base = apiAlerts.length > 0 ? apiAlerts : fallbackAlertsForEmployee;
return base.map(a => (readIds.has(a.id) ? { ...a, read: true } : a));
}, [apiAlerts, fallbackAlertsForEmployee, readIds]);

const handleRead = (id: string) => {
setReadIds(prev => {
const next = new Set(prev);
next.add(id);
return next;
});
const unreadCount = alerts.filter(a => !a.read).length;
const bellIcon: 'notificationOn' | 'notificationOff' =
open || unreadCount > 0 ? 'notificationOn' : 'notificationOff';
const bellColor = open || unreadCount > 0 ? 'bg-red-400' : 'bg-black';

// μ•Œλ¦Ό 읽음 처리(μ„œλ²„ + 둜컬 동기화)
const handleRead = async (id: string) => {
try {
if (user?.id) await markAlertRead(user.id, id);
} finally {
setReadIds(prev => {
const next = new Set(prev);
next.add(id);
return next;
});
setApiAlerts(prev => prev.map(a => (a.id === id ? { ...a, read: true } : a)));
}
};

// role이 초기 undefined일 수 μžˆμ–΄ λ°©μ–΄
const currentRole: UserRole = (role ?? 'guest') as UserRole;

// μ•„μ΄μ½˜μ€ "νŒ¨λ„ μ—΄λ¦Ό μƒνƒœ"둜만 ν† κΈ€
const bellIcon: 'notificationOn' | 'notificationOff' = open
? 'notificationOn'
: 'notificationOff';

return (
<nav className={cn('flex shrink-0 items-center gap-4 text-body-m font-bold', 'desktop:gap-10')}>
{(NAV_ITEMS[currentRole] ?? []).map(({ href, label }) => (
Expand All @@ -68,6 +105,7 @@ const Nav = () => {
</Link>
))}

{/* λ‘œκ·ΈμΈν•œ λˆ„κ΅¬λ‚˜ λ‘œκ·Έμ•„μ›ƒ λ…ΈμΆœ */}
{isLogin && (
<button
type='button'
Expand All @@ -79,34 +117,34 @@ const Nav = () => {
λ‘œκ·Έμ•„μ›ƒ
</button>
)}
{role === 'employee' && (

{/* βœ… 사μž₯λ‹˜(employer)μ—κ²Œλ§Œ μ•Œλ¦Ό λ²„νŠΌ μˆ¨κΉ€ */}
{isLogin && role !== 'employer' && (
<div className='relative'>
{/* μ•Œλ¦Ό λ²„νŠΌ: ν† κΈ€ */}
<button
type='button'
aria-label='μ•Œλ¦Ό ν™•μΈν•˜κΈ°'
aria-expanded={open}
aria-controls='notification-panel'
onClick={() => setOpen(prev => !prev)}
onClick={() => setOpen(v => !v)}
className='relative'
>
{/* 일뢀 λ©”λͺ¨μ΄μ œμ΄μ…˜ λŒ€λΉ„ κ°•μ œ λ¦¬λ Œλ” */}
<Icon
key={open ? 'bell-on' : 'bell-off'}
iconName={bellIcon}
iconSize='rg'
bigScreenSize='md'
ariaLabel='μ•Œλ¦Ό'
className={bellColor}
/>
</button>

{/* νŒ¨λ„: 열릴 λ•Œλ§Œ λ Œλ” + μ‚¬μ΄μ¦ˆ κ³ μ •(ν”Όκ·Έλ§ˆ) */}
{open && (
<Notification
alerts={alerts}
onRead={handleRead}
isOpen={open}
onClose={() => setOpen(false)} // λ‚΄λΆ€ 닫기와 연동
onClose={() => setOpen(false)}
/>
)}
</div>
Expand Down
17 changes: 15 additions & 2 deletions src/context/toastContext.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRouter } from 'next/router';
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

Expand All @@ -15,6 +16,7 @@ export const useToast = () => {

const ToastProvider = ({ children }: { children: ReactNode }) => {
const [message, setMessage] = useState<string | null>(null);
const router = useRouter();

const showToast = (msg: string) => {
setMessage(msg);
Expand All @@ -30,7 +32,18 @@ const ToastProvider = ({ children }: { children: ReactNode }) => {
return () => {
clearTimeout(timer);
};
});
}, [message]);

useEffect(() => {
const handleRouteChange = () => {
setMessage(null);
};

router.events.on('routeChangeStart', handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange);
};
}, [router]);

const toastRoot = typeof window !== 'undefined' ? document.getElementById('toast-root') : null;

Expand All @@ -43,7 +56,7 @@ const ToastProvider = ({ children }: { children: ReactNode }) => {
message ? (
<div
role='alert'
className='fixed top-[30%] left-1/2 z-[1] -translate-x-1/2 rounded-[5px] bg-red-300 px-4 py-[10px] text-white'
className='fixed left-1/2 top-[30%] z-[1] -translate-x-1/2 rounded-[5px] bg-red-300 px-4 py-[10px] text-white'
>
{message}
</div>
Expand Down
15 changes: 13 additions & 2 deletions src/pages/employer/shops/[shopId]/notices/register/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,23 @@ const EmployerNoticeRegisterPage = () => {
requiredMark
value={time ? { date: time, period: time.getHours() >= 12 ? 'μ˜€ν›„' : 'μ˜€μ „' } : null}
onChange={(value: TimeValue | null) => {
if (value && value.date < new Date()) {
if (!value) return;

const selectedDate = date ?? new Date();

const hours = value.date.getHours();
const minutes = value.date.getMinutes();

const combinedDateTime = new Date(selectedDate);
combinedDateTime.setHours(hours, minutes, 0, 0);

if (combinedDateTime < new Date()) {
setPastTimeModal(true);
setTime(null);
return;
}
setTime(value?.date ?? null);

setTime(combinedDateTime);
}}
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/my-profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function MyProfileDetailPage() {
return applications.map((app: ApiResponse<ApplicationItem>) => {
const a = app.item;
const status =
a.status === 'accepted' ? 'approved' : a.status === 'rejected' ? 'rejected' : 'pending';
a.status === 'accepted' ? 'accepted' : a.status === 'rejected' ? 'rejected' : 'pending';

return {
id: a.id,
Expand Down Expand Up @@ -187,7 +187,7 @@ export default function MyProfileDetailPage() {
title='μ‹ μ²­ λ‚΄μ—­'
content='λ§ˆμŒμ— λ“œλŠ” 곡고λ₯Ό μ°Ύμ•„ 지원해 λ³΄μ„Έμš”.'
buttonText='곡고 λ³΄λŸ¬κ°€κΈ°'
href='/notices'
href='/'
/>
) : (
<Container as='section' isPage className='pt-0'>
Expand Down
Loading