Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f7e7375
fix: 중첩 라우트에서 번들 경로 오류 수정
DongEun02 Aug 17, 2026
23c067e
style: text 컬러 white로 수정
DongEun02 Aug 17, 2026
e8589a1
fix: Button width 반응형으로 수정
DongEun02 Aug 17, 2026
0790739
feat: router에 경로 추가
DongEun02 Aug 17, 2026
7e7a0ee
feat: 이미지 추가 및 네이밍 변경
DongEun02 Aug 18, 2026
446c5c0
feat: camelCase로 수정
DongEun02 Aug 18, 2026
dda5272
feat: 공지 목록 페이지 구현
DongEun02 Aug 18, 2026
34ad22e
fix: 타입 오류 수정
DongEun02 Aug 18, 2026
a2dea89
Merge branch 'dev' into fe/feat/44-develop-notice-page
DongEun02 Aug 18, 2026
7756a47
style: fontStyle 수정 및 fontWeight 제거
DongEun02 Aug 18, 2026
c589bb6
feat: NoticeList 컴포넌트 경로 수정
DongEun02 Aug 18, 2026
7f39808
feat: 공지 작성 페이지 구현
DongEun02 Aug 18, 2026
90c053c
feat: 공지 수정 페이지 및 폼 컴포넌트 추가
DongEun02 Aug 18, 2026
88c257c
feat: 공지 상세 페이지 구현
DongEun02 Aug 18, 2026
f2ccb3c
feat: 공지 라우트 설정을 별도 파일로 분리
DongEun02 Aug 19, 2026
57ab67f
feat: 이미지 alt 속성 추가
DongEun02 Aug 19, 2026
ed8dc8a
feat: 공지 상세 페이지 및 목록 페이지 개선, 읽음 상태 표시 기능 추가
DongEun02 Aug 19, 2026
42b6088
fix: 불필요한 분기문 제거
DongEun02 Aug 19, 2026
e026dcd
Merge branch 'dev' into fe/feat/44-develop-notice-page
DongEun02 Aug 19, 2026
476205e
fix: url 경로 수정
DongEun02 Aug 19, 2026
dad1a66
fix: 중복된 alt 빈 문자열로 수정
DongEun02 Aug 19, 2026
801fe44
fix: 공지 목록에서 onSelect 제거 및 Link 컴포넌트로 변경
DongEun02 Aug 19, 2026
c4ddf74
fix: 읽기 진행 상태 업데이트 로직 수정 및 수정 경로 변경
DongEun02 Aug 19, 2026
335bf15
refactor: Main 컴포넌트 재사용
DongEun02 Aug 20, 2026
512dee9
style: 공용 컴포넌트 스타일 수정
DongEun02 Aug 20, 2026
1208944
refactor: 공지 관련 페이지 및 컴포넌트 리팩토링
DongEun02 Aug 20, 2026
af0442d
refactor: NoticeFormValues 인터페이스를 types.ts로 이동 및 경로 수정
DongEun02 Aug 20, 2026
1896dc8
style: 불필요한 marginBottom 제거
DongEun02 Aug 21, 2026
586fe94
feat: 공지 작성 버튼 클릭 핸들러 추가
DongEun02 Aug 21, 2026
cb0506b
feat: DateTimePicker에 id prop 추가 및 NoticeForm에서 사용
DongEun02 Aug 21, 2026
4f831d5
feat: 페이지 공용 레이아웃 컴포넌트 구현 후 적용
DongEun02 Aug 21, 2026
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
7 changes: 2 additions & 5 deletions frontend/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,16 @@ import { createBrowserRouter, RouterProvider } from 'react-router';
import { Global } from '@emotion/react';
import { globalStyles } from './src/styles/global';
import App from './src/App';
import NoticeListPage from './src/features/notice/NoticeListPage';
import { routes as noticeRoutes } from './src/features/notice/routes/route';
import { routes as studiesRoutes } from './src/features/studies/routes';

const router = createBrowserRouter([
{
path: '/',
element: <App />,
},
{
path: '/studies/:studyId/notices',
element: <NoticeListPage />,
},
...studiesRoutes,
...noticeRoutes,
]);

const root = document.getElementById('root')!;
Expand Down
40 changes: 0 additions & 40 deletions frontend/src/features/notice/NoticeList.tsx

This file was deleted.

58 changes: 0 additions & 58 deletions frontend/src/features/notice/NoticeListPage.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions frontend/src/features/notice/components/LeaderNoticeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import clock from '../../../shared/assets/clock-black.svg';
import Badge from '../../../shared/ui/Badge';
import NoticeList, { type NoticeListItem } from './NoticeList';

interface LeaderNoticeListProps {
notices: NoticeListItem[];
studyId: string;
}

export default function LeaderNoticeList({ notices, studyId }: LeaderNoticeListProps) {
return (
<NoticeList notices={notices} studyId={studyId} detailSearch="?role=leader">
{(notice) => (
<>
<Badge variant="BrandOutline" size="Small">
{notice.readCount}/{notice.totalCount} 읽음
</Badge>
{notice.reminderText && (
<Badge variant="NeutralSolid" size="Small">
<img src={clock} alt="리마인드 시각" width={12} height={12} />
{notice.reminderText}
</Badge>
)}
</>
)}
</NoticeList>
);
}
19 changes: 19 additions & 0 deletions frontend/src/features/notice/components/MemberNoticeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Badge from '../../../shared/ui/Badge';
import NoticeList, { type NoticeListItem } from './NoticeList';

interface MemberNoticeListProps {
notices: NoticeListItem[];
studyId: string;
}

export default function MemberNoticeList({ notices, studyId }: MemberNoticeListProps) {
return (
<NoticeList notices={notices} studyId={studyId}>
{(notice) => (
<Badge variant={notice.isRead ? 'BrandOutline' : 'BrandSolid'} size="Small">
{notice.isRead ? '읽음' : '읽지 않음'}
</Badge>
)}
</NoticeList>
);
}
110 changes: 110 additions & 0 deletions frontend/src/features/notice/components/MemberNoticeReadState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { CSSProperties } from 'react';
import { useEffect, useState } from 'react';
import checkIcon from '../../../shared/assets/check.svg';
import circleCheckIcon from '../../../shared/assets/circle-check.svg';
import { tokens, typography } from '../../../styles/global';

interface MemberNoticeReadStateProps {
progress: number;
readAt?: string;
}

const footerStyle = {
position: 'sticky',
bottom: 0,
zIndex: 10,
display: 'flex',
minHeight: '85px',
alignItems: 'center',
justifyContent: 'center',
borderTop: tokens.border.neutral,
background: tokens.bg.default,
} satisfies CSSProperties;

const readingStyle = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: tokens.spacing[1],
} satisfies CSSProperties;

const guideStyle = {
...typography.subtitle,
color: tokens.text.brand,
} satisfies CSSProperties;

const progressStyle = {
...typography.footnote,
color: tokens.text.muted,
} satisfies CSSProperties;

const completedStyle = {
...typography.subtitle,
display: 'flex',
alignItems: 'center',
gap: tokens.spacing[1],
color: tokens.text.brand,
} satisfies CSSProperties;

const toastStyle = {
...typography.body,
position: 'fixed',
right: tokens.layout.gutter,
bottom: `calc(100px + ${tokens.layout.safeBottom})`,
left: tokens.layout.gutter,
zIndex: 20,
display: 'flex',
maxWidth: '350px',
height: '48px',
margin: '0 auto',
padding: `0 ${tokens.spacing[4]}`,
alignItems: 'center',
gap: tokens.spacing[2],
borderRadius: tokens.radius.md,
background: tokens.bg.brand,
color: tokens.text.onBrand,
} satisfies CSSProperties;

export default function MemberNoticeReadState({ progress, readAt }: MemberNoticeReadStateProps) {
const isCompleted = progress >= 100;
const [isToastDismissed, setIsToastDismissed] = useState(false);

useEffect(() => {
if (!isCompleted) return;

const timeoutId = window.setTimeout(() => setIsToastDismissed(true), 2000);

return () => window.clearTimeout(timeoutId);
}, [isCompleted]);

return (
<>
{isCompleted && !isToastDismissed && (
<div css={toastStyle} role="status">
<img
src={checkIcon}
alt=""
width={16}
height={16}
css={{ filter: 'brightness(0) invert(1)' }}
/>
읽음으로 표시했어요
</div>
)}

<footer css={footerStyle}>
{isCompleted ? (
<div css={completedStyle}>
<img src={circleCheckIcon} alt="읽음 완료" width={22} height={22} />
{readAt ? `${readAt}에 읽음` : '읽음 완료'}
</div>
) : (
<div css={readingStyle}>
<strong css={guideStyle}>끝까지 읽으면 읽음으로 표시돼요</strong>
<span css={progressStyle}>지금 {progress}% 읽었어요</span>
</div>
)}
</footer>
</>
);
}
63 changes: 63 additions & 0 deletions frontend/src/features/notice/components/NoticeArticle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { CSSProperties } from 'react';
import profileIcon from '../../../shared/assets/unknown-profile.svg';
import { tokens, typography } from '../../../styles/global';

interface NoticeArticleProps {
title: string;
author: string;
createdAt: string;
content: string;
hasTopMargin?: boolean;
}

const articleStyle = {
display: 'flex',
flexDirection: 'column',
marginTop: tokens.spacing[5],
} satisfies CSSProperties;

const titleStyle = {
...typography.headline,
margin: 0,
color: tokens.text.primary,
} satisfies CSSProperties;

const authorRowStyle = {
display: 'flex',
alignItems: 'center',
gap: tokens.spacing[2],
marginTop: tokens.spacing[1],
} satisfies CSSProperties;

const authorStyle = {
...typography.footnote,
color: tokens.text.muted,
} satisfies CSSProperties;

const contentStyle = {
...typography.subtitle,
margin: `${tokens.spacing[4]} 0 0`,
color: tokens.text.secondary,
whiteSpace: 'pre-line',
} satisfies CSSProperties;

export default function NoticeArticle({
title,
author,
createdAt,
content,
hasTopMargin = true,
}: NoticeArticleProps) {
return (
<article css={{ ...articleStyle, marginTop: hasTopMargin ? articleStyle.marginTop : 0 }}>
<h2 css={titleStyle}>{title}</h2>
<div css={authorRowStyle}>
<img src={profileIcon} alt="" width={28} height={28} />
<span css={authorStyle}>
{author} · {createdAt}
</span>
</div>
<p css={contentStyle}>{content}</p>
</article>
);
}
41 changes: 41 additions & 0 deletions frontend/src/features/notice/components/NoticeDetailActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { CSSProperties } from 'react';
import deleteIcon from '../../../shared/assets/delete.svg';
import modifyIcon from '../../../shared/assets/modify.svg';
import Button from '../../../shared/ui/Button';
import { tokens } from '../../../styles/global';

interface NoticeDetailActionsProps {
onEdit: () => void;
onDelete: () => void;
}

const actionsStyle = {
display: 'flex',
gap: tokens.spacing[2],
marginTop: tokens.spacing[5],
} satisfies CSSProperties;

const buttonContentStyle = {
display: 'inline-flex',
alignItems: 'center',
gap: tokens.spacing[1],
} satisfies CSSProperties;

export default function NoticeDetailActions({ onEdit, onDelete }: NoticeDetailActionsProps) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 액션 부분을 분리한 특별한 이유가 있을까요 ~ ?

return (
<div css={actionsStyle}>
<Button variant="neutralOutline" size="small" onClick={onEdit}>
<span css={{ ...buttonContentStyle, color: tokens.text.primary }}>
<img src={modifyIcon} alt="" width={18} height={18} />
수정
</span>
</Button>
<Button variant="criticalOutline" size="small" onClick={onDelete}>
<span css={buttonContentStyle}>
<img src={deleteIcon} alt="" width={18} height={18} />
삭제
</span>
</Button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
);
}
Loading
Loading