[Feat] 회의록 페이지 및 작성 기능 구현 - #24
Conversation
📝 WalkthroughWalkthrough회의록 목록과 작성 화면이 추가되었고, 워크스페이스별 목업 회의록/멤버 조회, 상태 저장, 카드 렌더링, 라우트 연결이 함께 구성되었다. 공유 폰트 설정이 도입되어 일부 페이지가 해당 폰트를 사용하도록 변경되었다. Changes회의록 기능
공유 폰트
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant MeetingNotesPageRoute
participant MeetingNotesPage
participant MeetingNotesList
participant useMeetingNotesStore
participant MeetingNoteCard
User->>MeetingNotesPageRoute: /workspaces/{workspaceId}/meeting-notes 접속
MeetingNotesPageRoute->>MeetingNotesPage: workspaceId 전달
MeetingNotesPage->>MeetingNotesPage: getMockMeetingNotesByWorkspaceId(workspaceId)
MeetingNotesPage->>MeetingNotesList: meetingNotes, workspaceId 전달
MeetingNotesList->>useMeetingNotesStore: initializeWorkspace(workspaceId, meetingNotes)
MeetingNotesList->>MeetingNoteCard: 회의록 카드 렌더링
MeetingNoteCard-->>User: 제목/참석자/세부 항목 표시
sequenceDiagram
participant User
participant NewMeetingNotePageRoute
participant NewMeetingNotePage
participant MeetingNoteForm
participant useMeetingNotesStore
participant Router
User->>NewMeetingNotePageRoute: /workspaces/{workspaceId}/meeting-notes/new 접속
NewMeetingNotePageRoute->>NewMeetingNotePage: workspaceId 전달
NewMeetingNotePage->>MeetingNoteForm: workspaceId 전달
User->>MeetingNoteForm: 제목/날짜/참석자/내용 입력
User->>MeetingNoteForm: 제출
MeetingNoteForm->>useMeetingNotesStore: addMeetingNote(workspaceId, meetingNote)
MeetingNoteForm->>Router: /workspaces/{workspaceId}/meeting-notes 이동
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/entities/meeting-note/model/meeting-note.types.ts`:
- Around line 18-24: `MeetingNoteFormValues`의 `participants` 필드는
`MeetingNoteForm.tsx`에서 실제로 읽히지 않고 `selectedParticipantIds`와 제출 시 생성되는
`participants` 배열로만 처리되므로, 이 불필요한 상태를 타입에서 제거하거나 `MeetingNoteForm`의 실제 입력값과
연결되도록 정리하세요. 특히 `MeetingNoteFormValues`와 이를 사용하는 폼 초기값/제출 로직을 함께 확인해,
`participants`가 진짜 폼 데이터로 쓰이게 할지 아니면 타입 정의에서 없앨지 일관되게 맞추세요.
In `@src/entities/meeting-note/ui/MeetingNoteCard.tsx`:
- Around line 16-78: The clickable wrapper in MeetingNoteCard is invalid because
a <button> is containing <article> and <h3>, which are not allowed content for
that element. Update the outer interactive container in MeetingNoteCard to use a
non-button element such as a div with role="button" and tabIndex={0}, and add
keyboard support for Enter and Space while preserving the existing onClick and
aria-expanded behavior. Keep the internal article/heading structure unchanged so
the card remains accessible and semantically correct.
In `@src/features/manage-meeting-notes/ui/MeetingNoteForm.tsx`:
- Around line 302-313: The hidden native date input in MeetingNoteForm is still
exposed to assistive technology despite being visually hidden and removed from
tab order. Update the input rendered in MeetingNoteForm to include
aria-hidden="true" alongside the existing ref, type, value, onChange, className,
aria-label, and tabIndex settings so the custom 3-part date fields remain the
only accessible control.
- Around line 15-21: The MeetingNoteForm state currently keeps a redundant
participants string in formValues that is only updated by toggleParticipant and
never used in the save flow, while selectedParticipantIds/selectedParticipants
already drive the real MeetingNoteParticipant payload. Remove the unused
formValues.participants field and any related string-join updates, and keep
participant selection logic centered on the existing selectedParticipantIds and
selectedParticipants handling so the form has a single source of truth.
- Around line 15-21: The default meeting date in MeetingNoteForm is hardcoded to
an old fixed value, so new notes always start with that date instead of today.
Update the defaultFormValues in MeetingNoteForm.tsx so meetingDate is
initialized dynamically to the current date when the form is created, keeping
the existing form fields and MeetingNoteFormValues structure unchanged.
- Around line 98-104: In MeetingNoteForm, the submit flow currently never
reaches handleSubmit when the title is empty because the save action is disabled
by isTitleValid, so the hasSubmitted-based title error can never appear. Update
the form behavior so submission can occur and validation happens inside
handleSubmit (keeping setHasSubmitted(true) and the !formValues.title.trim()
check), or alternatively remove the unreachable inline error logic tied to
hasSubmitted && !isTitleValid; make the save control and validation state
consistent around handleSubmit, isTitleValid, and the 저장하기 button.
- Around line 114-119: The MeetingNoteForm.tsx meetingNoteId generation
currently uses only workspaceId, meetingDate, and the trimmed title slug, so
identical titles on the same date can collide. Update the meetingNoteId
construction in MeetingNoteForm to include an additional unique discriminator
from the form or generated note state (for example an existing note identifier
or a newly generated unique suffix) so store entries and React list keys remain
unique even when title and date match.
In `@src/views/meeting-notes/ui/NewMeetingNotePage.tsx`:
- Around line 4-7: The font is being instantiated directly inside
NewMeetingNotePage via Plus_Jakarta_Sans, which can lead to duplicated
next/font/google setup across meeting-notes views. Move the font loading to the
shared layout (layout.tsx) so it is created once and applied globally, and then
remove the per-view font initialization here and in any similar components like
MeetingNotesPage to keep styling consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: dc06e86c-4b69-4b13-93ac-902f0dda6bd9
📒 Files selected for processing (15)
src/app/workspaces/[workspaceId]/meeting-notes/new/page.tsxsrc/app/workspaces/[workspaceId]/meeting-notes/page.tsxsrc/entities/meeting-note/index.tssrc/entities/meeting-note/model/meeting-note.types.tssrc/entities/meeting-note/model/mock-meeting-notes-by-workspace.tssrc/entities/meeting-note/ui/MeetingNoteCard.tsxsrc/entities/workspace-member/index.tssrc/entities/workspace-member/model/mock-workspace-members.tssrc/features/manage-meeting-notes/index.tssrc/features/manage-meeting-notes/model/use-meeting-notes-store.tssrc/features/manage-meeting-notes/ui/MeetingNoteForm.tsxsrc/features/manage-meeting-notes/ui/MeetingNotesList.tsxsrc/views/meeting-notes/index.tssrc/views/meeting-notes/ui/MeetingNotesPage.tsxsrc/views/meeting-notes/ui/NewMeetingNotePage.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/entities/meeting-note/ui/MeetingNoteCard.tsx (2)
61-91: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
decisions/followUpActions문자열을 Reactkey로 그대로 사용 — 중복 시 충돌 위험
key={decision},key={actionItem}은 배열 값 자체를 키로 씁니다. 동일한 텍스트가 두 번 이상 등장하면(예: 같은 결정사항을 반복 기록)key충돌로 React 리스트 재조정이 부정확해지거나 콘솔 경고가 발생할 수 있습니다. 인덱스를 조합한 키가 더 안전합니다.🔑 제안 수정
- {meetingNote.decisions.map((decision) => ( - <li key={decision} className="flex items-start gap-2 text-[14px] leading-5 text-brand-ink"> + {meetingNote.decisions.map((decision, index) => ( + <li key={`${decision}-${index}`} className="flex items-start gap-2 text-[14px] leading-5 text-brand-ink"> <Check className="mt-[3px] size-[14px] shrink-0 text-[`#00c950`]" /> <span>{decision}</span> </li> ))}- {meetingNote.followUpActions.map((actionItem) => ( - <li key={actionItem} className="flex items-start gap-2 text-[14px] leading-5 text-brand-ink"> + {meetingNote.followUpActions.map((actionItem, index) => ( + <li key={`${actionItem}-${index}`} className="flex items-start gap-2 text-[14px] leading-5 text-brand-ink"> <ArrowRight className="mt-[3px] size-[14px] shrink-0 text-brand-start" /> <span>{actionItem}</span> </li> ))}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/entities/meeting-note/ui/MeetingNoteCard.tsx` around lines 61 - 91, In MeetingNoteCard’s expanded lists, the current React keys use the raw decision/action text, which can collide when duplicate strings appear. Update the list rendering for meetingNote.decisions and meetingNote.followUpActions to use a stable unique key per item, such as combining the item value with its index or another identifier, so the rendered lists in MeetingNoteCard remain deterministic even with repeated entries.
46-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win참석자 아바타의
aria-label이genericrole(div)에는 적용되지 않음
div는 명시적 role이 없으면 ARIAgenericrole로 매핑되며, ARIA 스펙상genericrole은 "Name from: Prohibited"이므로aria-label이 접근성 트리에 노출되지 않을 수 있습니다(브라우저별 동작이 비표준적으로 다름).title속성도 스크린리더에서 신뢰성 있게 읽히지 않으므로, 참석자 이름을 실제로 전달하려면role="img"를 부여하거나 시각적으로 숨긴 텍스트를 함께 넣는 방식을 권장합니다.♿️ 제안 수정
<div key={participant.id} + role="img" className="flex size-7 items-center justify-center rounded-full text-[12px] font-semibold text-white" style={{ backgroundColor: participant.color }} title={participant.name} aria-label={participant.name} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/entities/meeting-note/ui/MeetingNoteCard.tsx` around lines 46 - 58, The participant avatar divs in MeetingNoteCard are using aria-label on the default generic role, which may not expose the name to assistive tech. Update the participant element rendering in MeetingNoteCard to use an accessible pattern such as giving each avatar a non-generic role like img or adding visually hidden text with the participant.name, and keep the unique participant.id/key and existing participant fields intact while making the accessible name reliable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/manage-meeting-notes/ui/MeetingNoteForm.tsx`:
- Around line 15-24: The default meeting date is being computed once at module
load via getTodayIsoDate() and initialMeetingDate, so it can become stale if the
app stays open; move this to the MeetingNoteForm component and use a useState
lazy initializer so getTodayIsoDate() is recalculated on each mount. Update the
state setup that consumes initialMeetingDate to call getTodayIsoDate() directly
during initialization, keeping the date fresh when a new note form opens.
---
Outside diff comments:
In `@src/entities/meeting-note/ui/MeetingNoteCard.tsx`:
- Around line 61-91: In MeetingNoteCard’s expanded lists, the current React keys
use the raw decision/action text, which can collide when duplicate strings
appear. Update the list rendering for meetingNote.decisions and
meetingNote.followUpActions to use a stable unique key per item, such as
combining the item value with its index or another identifier, so the rendered
lists in MeetingNoteCard remain deterministic even with repeated entries.
- Around line 46-58: The participant avatar divs in MeetingNoteCard are using
aria-label on the default generic role, which may not expose the name to
assistive tech. Update the participant element rendering in MeetingNoteCard to
use an accessible pattern such as giving each avatar a non-generic role like img
or adding visually hidden text with the participant.name, and keep the unique
participant.id/key and existing participant fields intact while making the
accessible name reliable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2a3b7765-3367-46d4-9227-134cef9e2b78
📒 Files selected for processing (7)
src/entities/meeting-note/model/meeting-note.types.tssrc/entities/meeting-note/ui/MeetingNoteCard.tsxsrc/features/manage-meeting-notes/ui/MeetingNoteForm.tsxsrc/shared/lib/fonts.tssrc/views/meeting-notes/ui/MeetingNotesPage.tsxsrc/views/meeting-notes/ui/NewMeetingNotePage.tsxsrc/views/project-management/ui/ProjectManagementPage.tsx
💤 Files with no reviewable changes (1)
- src/entities/meeting-note/model/meeting-note.types.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/features/manage-meeting-notes/ui/MeetingNoteForm.tsx (2)
372-398: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win참석자 드롭다운에 ARIA 상태 속성 부재
토글 버튼(Line 375-398)에
aria-haspopup/aria-expanded가 없고, 패널의 멤버 행(Line 400-436)에도role="option"/aria-selected가 없습니다. 마우스·탭 키로는 동작하지만, 스크린리더 사용자는 드롭다운이 열려 있는지, 어떤 항목이 선택되어 있는지 인지하기 어렵습니다.♿ 제안 수정
<button type="button" onClick={() => setIsParticipantListOpen((current) => !current)} + aria-haspopup="listbox" + aria-expanded={isParticipantListOpen} className={`${fieldClassName} flex min-h-[58px] items-center justify-between gap-3 text-left`} ><button key={member.userId} type="button" + role="option" + aria-selected={isSelected} onClick={() => toggleParticipant(member.userId)} className="flex w-full items-center justify-between rounded-[14px] px-3 py-3 text-left transition hover:bg-brand-soft" >Also applies to: 400-436
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/manage-meeting-notes/ui/MeetingNoteForm.tsx` around lines 372 - 398, The participant dropdown is missing ARIA state and selection semantics, making its open/selected state unclear to screen readers. Update the toggle button in MeetingNoteForm to expose dropdown behavior with aria-haspopup and aria-expanded tied to isParticipantListOpen, and add appropriate listbox/option semantics to the participant panel and member rows, including role="option" and aria-selected based on whether each member is in selectedParticipants. Use the existing participantFieldRef, isParticipantListOpen, and selectedParticipants logic to keep the accessibility state in sync.
116-150: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win제출 시 날짜 재검증 추가 필요
handleSubmit에서 제목만 확인하고dateParts는 다시 검증하지 않습니다.handleDatePartBlur만 invalid 조합을 되돌리기 때문에, 유효하지 않은 날짜 입력 상태에서 바로 제출하면 화면에 보이던 값과 다른 이전meetingDate가 저장될 수 있습니다. 제출 시getIsoDateFromParts(...)로 날짜를 다시 확인하고, 유효한 값만 저장하도록 막아주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/manage-meeting-notes/ui/MeetingNoteForm.tsx` around lines 116 - 150, The submit handler in MeetingNoteForm.handleSubmit only validates the title, so invalid date parts can still be saved as an older meetingDate value. Re-check the current date parts at submit time using getIsoDateFromParts (the same logic used by handleDatePartBlur), and block addMeetingNote unless the assembled date is valid. Keep the existing participant mapping and router.push flow unchanged, but ensure only a validated ISO date is written into the meeting note payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/features/manage-meeting-notes/ui/MeetingNoteForm.tsx`:
- Around line 372-398: The participant dropdown is missing ARIA state and
selection semantics, making its open/selected state unclear to screen readers.
Update the toggle button in MeetingNoteForm to expose dropdown behavior with
aria-haspopup and aria-expanded tied to isParticipantListOpen, and add
appropriate listbox/option semantics to the participant panel and member rows,
including role="option" and aria-selected based on whether each member is in
selectedParticipants. Use the existing participantFieldRef,
isParticipantListOpen, and selectedParticipants logic to keep the accessibility
state in sync.
- Around line 116-150: The submit handler in MeetingNoteForm.handleSubmit only
validates the title, so invalid date parts can still be saved as an older
meetingDate value. Re-check the current date parts at submit time using
getIsoDateFromParts (the same logic used by handleDatePartBlur), and block
addMeetingNote unless the assembled date is valid. Keep the existing participant
mapping and router.push flow unchanged, but ensure only a validated ISO date is
written into the meeting note payload.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: bfe9cbdd-6ecc-4a54-afcd-854960bdea9c
📒 Files selected for processing (1)
src/features/manage-meeting-notes/ui/MeetingNoteForm.tsx
Pull Request
작업 내용
app / views / features / entities)로 분리해 추가했습니다.결정사항 / 후속 업무가 펼쳐지도록 구현했습니다.작업 결과
http://localhost:3000/workspaces/test/meeting-notes에서 회의록 목록 페이지를 확인할 수 있습니다.http://localhost:3000/workspaces/test/meeting-notes/new에서 회의록 작성 페이지를 확인할 수 있습니다.스크린샷
변경 사항
Added
entities/meeting-note타입, mock 데이터, 카드 UI 추가features/manage-meeting-notes목록/작성 UI 및 임시 저장 store 추가views/meeting-notes페이지 조합 추가test워크스페이스용 mock 멤버 데이터 추가Changed
연도 / 월 / 일직접 입력 + 달력 버튼 방식으로 변경결정사항[],후속 업무[]기준으로 정리Fixed
테스트
리뷰 체크리스트
feature/*->develop, 배포 시develop또는release/*->main)Type/#issue-number/description형식을 따릅니다.console.log, 주석, 임시 코드를 제거했습니다.리뷰 요청사항
관련 이슈
Closes #21
Summary by CodeRabbit