Skip to content

feat: 약속 회고 결과 페이지 UI 및 기능 개발 (#93)#102

Merged
mgYang53 merged 6 commits intodevelopfrom
feat/meeting-retro-result-93
Mar 2, 2026
Merged

feat: 약속 회고 결과 페이지 UI 및 기능 개발 (#93)#102
mgYang53 merged 6 commits intodevelopfrom
feat/meeting-retro-result-93

Conversation

@mgYang53
Copy link
Contributor

@mgYang53 mgYang53 commented Feb 26, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

AI 요약 결과를 조회·수정·발행하는 약속 회고 결과 페이지를 구현했습니다.

🔧 변경 사항

  • MeetingRetrospectiveResultPage: AI 요약 결과 조회, 수정 모드, 발행 기능 구현
  • TopicSummaryCard: 토픽별 핵심요약(Textarea) + 주요포인트(구조화된 개별 input) 편집 UI
    • 포인트/상세 항목 추가·삭제 지원
    • 하나의 border 박스 안에 통합된 입력 스타일
  • FormPageHeader: children prop으로 커스텀 액션 영역 지원, onBack prop 추가
  • SummaryInfoBanner: AI 요약 완료 안내 배너 컴포넌트
  • AiLoadingOverlay: SVG 직접 import 방식으로 변경 (AiGradientIcon 컴포넌트 삭제)
  • MeetingRetrospectiveCreatePage: STT 호출 로직 통합 (PR [feat] 약속회고 생성 전 플로우 UI 및 기능 개발 #100 UI + STT mutation 병합)
  • 에러 코드 추가 및 사전 의견 버튼 disabled 처리
  • summary 관련 API mock 데이터 추가
  • 발행 성공 시 DetailPage로 이동
  • 수정 모드에서 뒤로가기 시 편집 취소 (결과 페이지로 복귀)

📸 스크린샷 (선택 사항)

스크린샷 2026-02-27 오전 12 27 48 스크린샷 2026-02-27 오전 12 17 22

📄 기타

관련 이슈: #93
PR #100 병합 후 rebase하여 CreatePage UI와 STT 로직을 통합했습니다.

수정 모드 구현 참고

수정 API의 요청 형식이 { title: string, details: string[] }[] 구조로 고정되어 있어, 디자인의 단일 Textarea 방식(자유 텍스트 입력)으로는 파싱 신뢰성 문제가 발생합니다. 이에 따라 주요포인트 수정 영역을 구조화된 개별 input으로 구현하되, 하나의 border 박스 안에 통합하여 시각적으로 단일 입력 영역처럼 보이도록 처리했습니다. 포인트 및 상세 항목의 추가·삭제도 지원합니다.

스크린샷 2026-02-27 오전 12 23 47

Summary by CodeRabbit

  • New Features

    • 회고 요약 결과 페이지 추가: 요약 조회·편집·저장·공개 기능 제공.
    • 주제별 요약 카드 추가: 핵심 항목과 상세를 직접 편집/추가/삭제 가능.
    • AI 요약 완료 배너 추가: 완료 메시지 표시.
    • STT 기반 AI 요약 시작 흐름 및 로딩 오버레이 추가.
  • Refactor

    • 라우트·페이지 교체: 기존 회고 페이지를 결과 페이지로 대체.
    • 아이콘 사용 단순화: 인라인 SVG 컴포넌트 제거 후 이미지 리소스로 대체.
    • 폼 헤더 액션 API 개선: 액션 영역의 유연성 강화.
  • Chores

    • 로컬 모킹 확장: 요약 관련 모의 데이터와 CRUD 지원 추가.

- AI 요약 결과 조회, 수정, 발행 기능 구현
- TopicSummaryCard: 구조화된 개별 input으로 주요포인트 편집
- FormPageHeader: children, onBack prop 추가
- 발행 성공 시 DetailPage로 이동
- AiLoadingOverlay SVG 직접 import 방식으로 변경
- summary 관련 목데이터 추가
@mgYang53 mgYang53 linked an issue Feb 26, 2026 that may be closed by this pull request
7 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b40d6e and e2fc1ce.

📒 Files selected for processing (2)
  • src/pages/Retrospectives/index.ts
  • src/routes/index.tsx

Walkthrough

AI 요약 워크플로우를 추가합니다. STT 작업 훅(useCreateSttJob)과 모의 API/데이터를 도입하고, 요약 관련 UI(요약 배너 SummaryInfoBanner, 주제별 편집 카드 TopicSummaryCard)와 결과 페이지를 추가·교체하며 인라인 SVG 아이콘을 외부 이미지로 교체하고 FormPageHeader의 props 타입을 엄격화합니다.

Changes

Cohort / File(s) Summary
아이콘 교체
src/features/retrospectives/components/AiGradientIcon.tsx, src/features/retrospectives/components/AiLoadingOverlay.tsx
인라인 SVG 컴포넌트(AiGradientIcon) 삭제 및 AiLoadingOverlay에서 외부 SVG 에셋(img)으로 교체. 주의: AiGradientIcon 삭제로 같은 아이콘을 참조하던 다른 사용처 검사 필요.
요약 UI 컴포넌트 추가
src/features/retrospectives/components/SummaryInfoBanner.tsx, src/features/retrospectives/components/TopicSummaryCard.tsx, src/features/retrospectives/components/index.ts
요약 안내 배너와 주제별 편집/표시 카드 추가. TopicSummaryCard는 편집 모드와 표시 모드, keyPoint/detail 편집 로직 및 외부 onChange 콜백을 노출함(신규 타입 EditableDetail, EditableKeyPoint). 인덱스 파일에 새 컴포넌트/타입을 export.
API 및 모의 데이터 확장
src/features/retrospectives/retrospectives.api.ts, src/features/retrospectives/retrospectives.mock.ts
createSttJob mock에 2초 지연 및 AbortSignal 지원 추가, getSummary/updateSummary/publishSummary의 mock 브랜치 도입. 모의 요약 데이터 저장·갱신·발행 유틸(getMockSummary, mockUpdateSummary, mockPublishSummary) 추가.
페이지·라우트 재배치
src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx, src/pages/Retrospectives/MeetingRetrospectivePage.tsx, src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx, src/pages/Retrospectives/index.ts, src/routes/index.tsx
Create 페이지에 useCreateSttJob 훅 연결 및 AiLoadingOverlay 바인딩; 기존 MeetingRetrospectivePage 삭제 및 MeetingRetrospectiveResultPage 추가로 라우트/바렐 교체. 네비게이션 시 fromAiSummary 플래그 사용.
헤더 API 변경
src/shared/components/FormPageHeader.tsx
FormPageHeader props를 엄격한 union 타입으로 재구성(액션 변형 vs children 변형), onBack 로직 보완, 기본 액션 영역을 children으로 대체 가능. 소비자 컴포넌트들에서 호출 시 타입 불일치 주의.
기타 타입·내보내기
src/features/retrospectives/...
retrospectives feature에 useCreateSttJob 훅 및 summary 관련 mock/헬퍼 함수들의 공개 내보내기 추가. 기존 훅/타입 사용처와 호환성 확인 필요.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CreatePage as MeetingRetrospectiveCreatePage
    participant API as retrospectives.api
    participant ResultPage as MeetingRetrospectiveResultPage
    participant Backend as Backend

    User->>CreatePage: AI 요약 시작(파일/IDs)
    CreatePage->>API: createSttJob(req, signal)
    alt USE_MOCK
        API->>API: 2초 지연(AbortSignal 처리)
        API-->>CreatePage: SttJobResponse(status: DONE)
    else 실제 API
        API->>Backend: STT 작업 생성 요청
        Backend-->>API: 작업 응답
        API-->>CreatePage: SttJobResponse
    end
    CreatePage->>ResultPage: navigate(..., { fromAiSummary: true })
    ResultPage->>API: getSummary(meetingId)
    API-->>ResultPage: RetrospectiveSummaryResponse
    User->>ResultPage: 편집/추가/삭제
    ResultPage->>API: updateSummary(meetingId, data)
    API-->>ResultPage: 업데이트된 요약
    User->>ResultPage: 발행
    ResultPage->>API: publishSummary(meetingId)
    API-->>ResultPage: 발행 완료
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • 이슈 #93: AI 요약 워크플로우(TopicSummaryCard, SummaryInfoBanner, useCreateSttJob, 결과 페이지 등)를 구현하므로 코드 수준에서 목적이 일치합니다.

Possibly related PRs

  • PR #88: FormPageHeader 최초/주요 변경과 겹치므로 props 타입 변경 충돌 여부 확인 필요.
  • PR #95: retrospectives API/훅 및 요약 저장·갱신 흐름을 공유하므로 논리·API 호환성 관련 연계가 높음.
  • PR #102: 아이콘/컴포넌트 삭제·교체, 요약 관련 컴포넌트 및 페이지 변경이 유사하므로 중복/충돌 가능성이 큼.

Suggested labels

feat

Suggested reviewers

  • haruyam15
  • choiyoungae
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 요약하고 있습니다. 약속 회고 결과 페이지의 UI와 기능 개발이라는 핵심 내용이 잘 반영되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/meeting-retro-result-93

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/shared/components/FormPageHeader.tsx (1)

12-15: JSDoc과 타입 정의가 불일치합니다.

주석에 "children 미사용 시 필수"라고 명시되어 있지만, 타입은 optional입니다. → 실제로 둘 다 누락해도 컴파일 에러가 발생하지 않아 액션 영역이 비어버릴 수 있습니다. → 런타임 경고를 추가하거나, 타입 레벨에서 children vs actionLabel+onAction 중 하나를 강제하는 discriminated union을 고려해보세요.

🔧 타입 레벨 강제 예시 (discriminated union)
-export interface FormPageHeaderProps {
-  /** 페이지 제목 */
-  title: string
-  /** 액션 버튼 텍스트 (children 미사용 시 필수) */
-  actionLabel?: string
-  /** 액션 버튼 클릭 핸들러 (children 미사용 시 필수) */
-  onAction?: () => void
+interface FormPageHeaderBaseProps {
+  title: string
   isActionDisabled?: boolean
   to?: string
   onBack?: () => void
   className?: string
-  children?: React.ReactNode
 }
+
+interface FormPageHeaderWithAction extends FormPageHeaderBaseProps {
+  actionLabel: string
+  onAction: () => void
+  children?: never
+}
+
+interface FormPageHeaderWithChildren extends FormPageHeaderBaseProps {
+  actionLabel?: never
+  onAction?: never
+  children: React.ReactNode
+}
+
+export type FormPageHeaderProps = FormPageHeaderWithAction | FormPageHeaderWithChildren
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/FormPageHeader.tsx` around lines 12 - 15, The JSDoc
claims "actionLabel and onAction required when children is not used" but the
types for FormPageHeader currently make actionLabel?: string and onAction?: ()
=> void optional, allowing an empty action area; update the props for the
FormPageHeader component to enforce at the type level by creating a
discriminated union between (a) props with children: ReactNode (no
actionLabel/onAction required) and (b) props without children that require
actionLabel: string and onAction: () => void, or alternatively keep current
types but add a runtime check in FormPageHeader (checking children vs
actionLabel/onAction) that throws or logs a clear warning if both are missing;
reference the prop names actionLabel, onAction, children and the component
FormPageHeader when making the change so code consumers and the component
implementation are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/retrospectives/components/TopicSummaryCard.tsx`:
- Around line 100-101: The map over displayKeyPoints uses the array index
(kpIndex) as the React key which causes input/focus mismatches when items are
added/removed; update the local state shape for key points to include a stable
temporary id (e.g., id or uuid) when items are created/initialized, change the
mapping to use that stable id as the key (replace key={kpIndex} with
key={kp.id}), and refactor any add/remove/update handlers in TopicSummaryCard
(and any functions that push new key points) to preserve and operate on the id;
when preparing payloads for the API, strip out the temporary id and send only
the required fields ({ title, details }) as the comment suggests.

In `@src/features/retrospectives/retrospectives.api.ts`:
- Around line 81-88: The mock abort path uses reject(new DOMException(...))
which yields error.message != 'canceled' so the UI treats it as a real error;
modify the USE_MOCK branch to reject with an Error whose message is 'canceled'
(or otherwise normalize to match axios abort behavior) when
signal?.addEventListener('abort'...) fires (the block around signal and the
timeout in retrospectives.api.ts), ensuring the rejection mirrors axios's
canceled error so the UI suppression logic works.

In `@src/features/retrospectives/retrospectives.mock.ts`:
- Around line 181-280: Current mock uses a single global mockSummaryData shared
across meetings; update it to a meeting-scoped map (Record<number,
RetrospectiveSummaryResponse>) and change getMockSummary, mockUpdateSummary, and
mockPublishSummary to accept a meetingId parameter and operate on the entry for
that meeting (create a cloned default entry if missing). Specifically, replace
the global mockSummaryData with something like mockSummaryByMeeting:
Record<number, RetrospectiveSummaryResponse>, then in getMockSummary(meetingId)
return structuredClone(mockSummaryByMeeting[meetingId] ||
defaultCloneFor(meetingId)); in mockUpdateSummary(meetingId, data) find and
update only mockSummaryByMeeting[meetingId].topics (preserve other meetings),
and in mockPublishSummary(meetingId) set isPublished and publishedAt on
mockSummaryByMeeting[meetingId]; ensure all returns still use structuredClone to
avoid shared references.

In `@src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx`:
- Around line 33-34: The component currently calls useSummary(mId) but only
destructures data and isLoading; update the hook call to also extract isError
(and error if available) from useSummary, add a render branch that shows an
error message and a retry action/component (e.g., using the existing Error/Retry
UI or a simple button that refetches) when isError is true, and change the
empty-state rendering so it only shows when summaryData is present and
summaryData.topics.length === 0; reference useSummary, summaryData, isLoading,
isError and the empty-state render path to locate where to add the new error
branch and the retry handler.
- Around line 46-51: Replace the direct history mutation in the useEffect that
checks fromAiSummary with a React Router navigation call: instead of
window.history.replaceState({}, ''), call the router navigate API (use the
navigate function from useNavigate) with the current location and options {
replace: true, state: null } so Router-managed location/state is cleared; keep
the existing showToast('독서 모임 내용 요약이 완료됐어요') and ensure useEffect depends on
fromAiSummary and that navigate is imported/available in the component.

---

Nitpick comments:
In `@src/shared/components/FormPageHeader.tsx`:
- Around line 12-15: The JSDoc claims "actionLabel and onAction required when
children is not used" but the types for FormPageHeader currently make
actionLabel?: string and onAction?: () => void optional, allowing an empty
action area; update the props for the FormPageHeader component to enforce at the
type level by creating a discriminated union between (a) props with children:
ReactNode (no actionLabel/onAction required) and (b) props without children that
require actionLabel: string and onAction: () => void, or alternatively keep
current types but add a runtime check in FormPageHeader (checking children vs
actionLabel/onAction) that throws or logs a clear warning if both are missing;
reference the prop names actionLabel, onAction, children and the component
FormPageHeader when making the change so code consumers and the component
implementation are updated accordingly.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3048221 and cd01c21.

⛔ Files ignored due to path filters (1)
  • src/shared/assets/icon/ai-gradient.svg is excluded by !**/*.svg, !**/*.svg
📒 Files selected for processing (13)
  • src/features/retrospectives/components/AiGradientIcon.tsx
  • src/features/retrospectives/components/AiLoadingOverlay.tsx
  • src/features/retrospectives/components/SummaryInfoBanner.tsx
  • src/features/retrospectives/components/TopicSummaryCard.tsx
  • src/features/retrospectives/components/index.ts
  • src/features/retrospectives/retrospectives.api.ts
  • src/features/retrospectives/retrospectives.mock.ts
  • src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx
  • src/pages/Retrospectives/MeetingRetrospectivePage.tsx
  • src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx
  • src/pages/Retrospectives/index.ts
  • src/routes/index.tsx
  • src/shared/components/FormPageHeader.tsx
💤 Files with no reviewable changes (2)
  • src/pages/Retrospectives/MeetingRetrospectivePage.tsx
  • src/features/retrospectives/components/AiGradientIcon.tsx

- TopicSummaryCard 편집 모드 keyPoints map key를 인덱스에서 stable id로 교체
  (EditableKeyPoint 타입 추가, crypto.randomUUID()로 id 생성)
- mock abort 에러를 DOMException에서 Error('canceled')로 변경해 axios 취소 동작 일치
- 회고 요약 목데이터를 전역 단일 객체에서 meetingId별 Record로 리팩터링
- MeetingRetrospectiveResultPage에 isError 처리 및 다시 시도 버튼 추가
- window.history.replaceState를 navigate(location.pathname, replace)로 교체
- FormPageHeader props를 discriminated union으로 타입 강화
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/features/retrospectives/components/TopicSummaryCard.tsx (1)

125-128: ⚠️ Potential issue | 🟠 Major

상세 항목에 index key를 쓰면 편집 중 입력값이 섞일 수 있습니다.

문제: Line 127에서 key={detailIndex}를 사용하고 있습니다.
영향: 상세 추가/삭제 시 DOM 재사용으로 포커스나 입력값이 다른 항목으로 이동할 수 있습니다.
대안: 상세 항목도 로컬에서 id를 부여한 구조({ id, value })로 관리하고, key/수정/삭제를 id 기준으로 처리한 뒤 저장 시 string[]로 직렬화하세요.

#!/bin/bash
# Verify index-key usage on editable detail rows in TopicSummaryCard.
rg -n -C2 "map\\(\\(detail, detailIndex\\)|key=\\{detailIndex\\}|handleAddDetail|handleRemoveDetail" src/features/retrospectives/components/TopicSummaryCard.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/retrospectives/components/TopicSummaryCard.tsx` around lines 125
- 128, The map over kp.details using key={detailIndex} in TopicSummaryCard (the
block mapping kp.details) can cause input/ focus mixups when rows are
added/removed; change the details data shape to objects like {id, value} and
update TopicSummaryCard's handlers (e.g., handleAddDetail, handleRemoveDetail,
and the edit/update logic that references detailIndex) to operate by id, use
detail.id as the React key in the JSX map, and on save/serialize convert the
array back to string[] so storage shape remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/retrospectives/components/index.ts`:
- Line 6: The export line uses "export { type EditableKeyPoint,default as
TopicSummaryCard } from './TopicSummaryCard'" which triggers Prettier due to the
missing space after the comma; update the export to include a space after the
comma so it reads "export { type EditableKeyPoint, default as TopicSummaryCard }
from './TopicSummaryCard'" (adjusting the export statement that references
EditableKeyPoint and TopicSummaryCard).

In `@src/features/retrospectives/retrospectives.api.ts`:
- Around line 82-88: In the mock STT wait Promise, first check signal?.aborted
and immediately reject if true, attach the abort listener with { once: true }
when calling signal.addEventListener to avoid accumulating listeners, and ensure
you clean up the listener when the timer completes by storing the listener
function and removing it (or using the once option) before resolving; keep
clearing the timeout on abort as-is and preserve rejecting with new
Error('canceled') to maintain existing behavior.

In `@src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx`:
- Around line 30-33: Validate meetingId after converting to number: replace the
naive Number(meetingId) usage that sets mId (and the similar usages around the
other occurrences) with an explicit check using Number.isInteger(mId) && mId > 0
before calling useSummary or rendering the summary UI; if validation fails,
short-circuit to an error state or perform a safe redirect/render an error
component instead of attempting to fetch/assuming empty data. Locate the
conversion and fetch call sites (the mId assignment and subsequent
useSummary(mId) invocation in MeetingRetrospectiveResultPage and the analogous
blocks at the other mentioned occurrences) and add the integer/positive guard,
branching to the error/redirect flow when the guard fails.

---

Duplicate comments:
In `@src/features/retrospectives/components/TopicSummaryCard.tsx`:
- Around line 125-128: The map over kp.details using key={detailIndex} in
TopicSummaryCard (the block mapping kp.details) can cause input/ focus mixups
when rows are added/removed; change the details data shape to objects like {id,
value} and update TopicSummaryCard's handlers (e.g., handleAddDetail,
handleRemoveDetail, and the edit/update logic that references detailIndex) to
operate by id, use detail.id as the React key in the JSX map, and on
save/serialize convert the array back to string[] so storage shape remains
unchanged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd01c21 and 3df81c4.

📒 Files selected for processing (6)
  • src/features/retrospectives/components/TopicSummaryCard.tsx
  • src/features/retrospectives/components/index.ts
  • src/features/retrospectives/retrospectives.api.ts
  • src/features/retrospectives/retrospectives.mock.ts
  • src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx
  • src/shared/components/FormPageHeader.tsx

- 상세 항목(details)에도 EditableDetail 타입으로 stable id 적용
  (추가/삭제 시 input 포커스 혼동 방지)
- handleSaveEdit에서 EditableDetail[] → string[]으로 직렬화 처리
- mock STT abort: aborted 즉시 감지, once 옵션으로 리스너 중복 방지, resolve 시 리스너 정리
- meetingId 유효성 검사 강화 (Number.isInteger && > 0)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx (1)

103-103: Non-null assertion 사용 검증 필요

문제: gatheringId!meetingId!에서 non-null assertion을 사용하고 있습니다.

영향: Line 128에서 조기 반환하므로 실제로는 안전하지만, 코드 가독성과 타입 안전성 측면에서 개선 여지가 있습니다.

대안: 조기 반환 후에는 타입이 좁혀지지 않으므로, validated 변수를 별도로 선언하거나 현재 방식을 유지해도 무방합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx` at line 103, The
call to navigate(ROUTES.MEETING_RETROSPECTIVE_DETAIL(gatheringId!, meetingId!))
uses non-null assertions on gatheringId and meetingId; replace them by
explicitly narrowing/validating the values before use: after the existing
early-return check that ensures both exist, assign validatedGatheringId =
gatheringId and validatedMeetingId = meetingId (or otherwise guard with if
(!gatheringId || !meetingId) return) and then call
navigate(ROUTES.MEETING_RETROSPECTIVE_DETAIL(validatedGatheringId,
validatedMeetingId)) to avoid the `!` assertions while preserving the same
control flow and types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx`:
- Around line 186-197: The build fails because topics is a union type
(SummaryTopic[] | EditableSummaryTopic[]) but TopicSummaryCard.topic expects
SummaryTopic; add a type guard or conditional mapping so you only pass
SummaryTopic to TopicSummaryCard: when rendering topics.map, check whether each
topic is a SummaryTopic (or whether isEditing is false) and only pass topic to
TopicSummaryCard if it satisfies SummaryTopic, otherwise either cast after a
runtime check or render a different component for EditableSummaryTopic; update
the map logic around topics, TopicSummaryCard, isEditing, editedTopics,
handleSummaryChange, and handleKeyPointsChange so the prop types align and
TS2322 is resolved.

---

Nitpick comments:
In `@src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx`:
- Line 103: The call to
navigate(ROUTES.MEETING_RETROSPECTIVE_DETAIL(gatheringId!, meetingId!)) uses
non-null assertions on gatheringId and meetingId; replace them by explicitly
narrowing/validating the values before use: after the existing early-return
check that ensures both exist, assign validatedGatheringId = gatheringId and
validatedMeetingId = meetingId (or otherwise guard with if (!gatheringId ||
!meetingId) return) and then call
navigate(ROUTES.MEETING_RETROSPECTIVE_DETAIL(validatedGatheringId,
validatedMeetingId)) to avoid the `!` assertions while preserving the same
control flow and types.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3df81c4 and f4869e2.

📒 Files selected for processing (4)
  • src/features/retrospectives/components/TopicSummaryCard.tsx
  • src/features/retrospectives/components/index.ts
  • src/features/retrospectives/retrospectives.api.ts
  • src/pages/Retrospectives/MeetingRetrospectiveResultPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/retrospectives/components/index.ts

- guard를 핸들러 정의 이전으로 이동해 gatheringId/meetingId non-null assertion 제거
- topic prop을 항상 SummaryTopic[]으로 유지해 union 타입 불일치 해결
- EditableDetail/EditableKeyPoint를 components/index.ts에서 함께 re-export
@mgYang53 mgYang53 merged commit ff88378 into develop Mar 2, 2026
1 of 2 checks passed
@mgYang53 mgYang53 deleted the feat/meeting-retro-result-93 branch March 2, 2026 13:36
mgYang53 added a commit that referenced this pull request Mar 7, 2026
* [refactor] 사전의견 폴더 병합 (#92)

* refactor: 사전의견 관련 폴더 병합 (#91)

* fix: SubPageHeader 그림자 prop 추가 (#91)

* refactor: 코드 포매팅 (#91)

* fix: 빌드 에러 수정 (#91)

* fix: 개인 회고 nullable 필드 처리 및 목 모드 조기 반환 추가 (#91)

* chore: 개인회고 관련 파일 삭제 (#91)

* feat: 약속 회고 API 레이어 구축 (#94) (#95)

* feat: 약속 회고 API 레이어 구축 (#94)

STT Job 생성, 요약 조회/수정/발행 API 함수 및 React Query 훅 구현.
AbortController 기반 STT 요청 취소 지원, setQueryData 캐시 전략 적용.

* style: prettier 포맷 수정 (#94)

* refactor: 코드 리뷰 반영 (#94)

- api 함수 api.* 헬퍼로 통일
- useCreateSttJob race condition 수정
- useCreateSttJob 언마운트 시 자동 취소
- KeyPointUpdateRequest 타입 alias로 변경

* feat: 에러 코드 추가 및 사전 의견 버튼 disabled 처리 (#97) (#98)

신규 에러 코드 5개(E000, M016, M017, R106, R108) 추가.
MyMeetingListItem에 preOpinionTemplateConfirmed 필드 추가 후,
메인페이지 예정 약속의 사전 의견 작성 버튼 disabled 처리 반영.

* [feat] 약속회고 생성 전 플로우 UI 및 기능 개발 (#100)

* feat: 약속 완료 시 약속 상세 페이지 UI 변경(#87)

* feat: 약속 회고 상세 페이지 추가 (#87)

* feat:아코디언 컴포넌트 추가(#87)

* design: 아코디언 컴포넌트css 수정(#87)

* feat: 수집된 사전의견 조회 api 개발(#87)

* feat: Dropzone 공통컴포넌트 생성 및 파일 업로드 기능구현(#87)

* design: AI요약 버튼 디자인 수정(#87)

* style: 프리티어(#87)

* refactor: 파일 업로드 검증 강화 및 의존성 정리 (#87)

Dropzone 파일 타입 검증 기능 추가 및 radix-ui 의존성 정리로 코드 품질 개선.

- Dropzone 파일 타입 검증 로직 추가
- 파일 타입 불일치 시 onTypeRejected 콜백 호출
- alert를 toast로 변경하여 일관된 에러 처리
- radix-ui 제거 후 @radix-ui/react-accordion 추가
- Accordion, AlertIcon 컴포넌트 코드 정리
- Import/export 경로 정리 및 불필요한 mock export 제거

* refactor: 에러처리 개선 (#87)

* design: 아코디언css 수정 (#87)

* refactor: GetCollectedAnswersParams 배럴 추가(#87)

* style: 주석제거(#87)

* refactor: AlertIcon 색 커스텀 기능 추가(#87)

* refactor: 회고 아이콘을 SVG 파일로 전환 (#87)

- MeetingRetroIcon, PersonalRetroIcon 컴포넌트를 SVG 파일로 대체
- useCollectedAnswers 훅의 유효성 검사 강화 (정수 체크, pageSize 검증)
- MeetingDetailPage 에러 처리 중복 실행 방지

* refactor: AlertIcon적용 (#87)

* refactor: AlertIcon size prop추가(#87)

* refactor: 주제 목록 에러 처리 개선 (#87)

* fix: 버튼 프로퍼티수정(#87)

* feat: 회고 상태 기반 라우트 분기 처리 추가 (#87)

* feat: 목데이터에 회고상태  추가(#87)

* refactor: params검증로직 페이지에서 처리(#87)

* [feat] 개인회고 작성 및 조회 화면 구현 (#101)

* feat: 개인 회고 작성 화면 구현 (#90)

* feat: 개인 회고 전송 기능 구현 (#90)

* feat: 개인회고 조회 화면 구현 (#90)

* feat: 개인회고 삭제, 수정 구현 (#90)

* chore 미사용 유틸리티 제거 (#90)

* refactor: 코드 포매팅 (#90)

* fix: 코드리뷰 반영 (#90)

* fix: 코드 리뷰 반영 (#90)

* refactor: 코드 포매팅 (#90)

* refactor: 개인회고 폴더 정리 (#90)

* fix: 코드 리뷰 반영 (#90)

* feat: 약속 회고 결과 페이지 UI 및 기능 개발 (#93) (#102)

* feat: 약속 회고 결과 페이지 UI 및 기능 개발 (#93)

- AI 요약 결과 조회, 수정, 발행 기능 구현
- TopicSummaryCard: 구조화된 개별 input으로 주요포인트 편집
- FormPageHeader: children, onBack prop 추가
- 발행 성공 시 DetailPage로 이동
- AiLoadingOverlay SVG 직접 import 방식으로 변경
- summary 관련 목데이터 추가

* fix: 코드 리뷰 반영 (#93)

- TopicSummaryCard 편집 모드 keyPoints map key를 인덱스에서 stable id로 교체
  (EditableKeyPoint 타입 추가, crypto.randomUUID()로 id 생성)
- mock abort 에러를 DOMException에서 Error('canceled')로 변경해 axios 취소 동작 일치
- 회고 요약 목데이터를 전역 단일 객체에서 meetingId별 Record로 리팩터링
- MeetingRetrospectiveResultPage에 isError 처리 및 다시 시도 버튼 추가
- window.history.replaceState를 navigate(location.pathname, replace)로 교체
- FormPageHeader props를 discriminated union으로 타입 강화

* style: prettier 포맷 수정 (#93)

* fix: 코드 리뷰 반영 2차 (#93)

- 상세 항목(details)에도 EditableDetail 타입으로 stable id 적용
  (추가/삭제 시 input 포커스 혼동 방지)
- handleSaveEdit에서 EditableDetail[] → string[]으로 직렬화 처리
- mock STT abort: aborted 즉시 감지, once 옵션으로 리스너 중복 방지, resolve 시 리스너 정리
- meetingId 유효성 검사 강화 (Number.isInteger && > 0)

* fix: 코드 리뷰 반영 3차 (#93)

- guard를 핸들러 정의 이전으로 이동해 gatheringId/meetingId non-null assertion 제거
- topic prop을 항상 SummaryTopic[]으로 유지해 union 타입 불일치 해결
- EditableDetail/EditableKeyPoint를 components/index.ts에서 함께 re-export

* [fix] 책 상세 페이지, 내 책장 페이지 API 스펙 반영 (#104)

* refactor: 감상 기록 액션 메뉴 분리 (#59)

* refactor: Book/Keywords API 목데이터와 엔드포인트 분리 (#59)

* design: 감상 기록 액션 메뉴 관련 z-index 조정 (#59)

* feat: 별점 필터링에 0점 추가 (#59)

* refactor: 내 책장 무한스크롤 시 useInfiniteScroll 활용 (#59)

* feat: 내 책장에 등록된 책이 없을 경우 툴팁 추가 (#59)

* feat: 기록 타임라인의 사전의견에 답변이 없는 주제 처리 추가 (#59)

* refactor: sticky한 헤더에 대해 drop-shadow-bottom 및 useScrollCollapse 활용 (#59)

* fix: 책 타임라인 api 반영 (#59)

* fix: 책 리스트 조회 api 반영 (#59)

* fix: 책 리스트 삭제 api 반영 및 디자인 수정 (#59)

* fix: 도서 API 스펙 변경에 따른 타입 및 컴포넌트 업데이트 (#59)

* fix: 약속 회고 아이템에서 미연동 액션 메뉴 제거 (#59)

* feat: 도서 읽기 상태 토글에 낙관적 업데이트 적용 (#59)

* refactor: 코드 포매팅 (#59)

* fix: 메인페이지 ReadingBooksSection API 파라미터 및 응답 필드 타입 오류 수정 (#59)

* fix: 내책장 편집 모드 진입 시 filteredBookIds 초기화 (#59)

* fix: mock API 파라미터 계약 및 라우트 상수 관련 수정 (#59)

* fix: PRE_OPINION 아이템 React key 중복 가능성 수정 (#59)

* refactor: RootLayout에서 불필요한 overflow-x-clip 제거 (#59)

* refactor: BookLogList 삭제 로직을 useBookLogDeleteActions 훅으로 분리 (#59)

* [feat] 약속회고 조회 및 댓글 기능 구현  (#103)

* feat: 약속 회고상세 UI개발(#96)

* refactor: 회고 관련 파일 구조 meeting 폴더로 재구성 (#96)

* feat: 댓글 Textarea 높이조절 기능구현(#96)

* feat: 약속회고 댓글 기능 구현 (#96)

- 약속회고 상세 페이지에 댓글 작성/삭제 기능 추가.
- 무한 스크롤로 댓글 목록 조회하며, 약속장 및 댓글 작성자만 삭제 가능.

* fix: 약속회고 댓글 에러 처리 및 안정성 개선 (#96)

- 댓글 조회 실패 시 에러 토스트 및 안내 메시지 표시.
- currentUserId null 체크로 삭제 권한 확인 로직 개선.
- 스켈레톤 count 음수값 방어 처리.
- Textarea format별 height 기본값 적용.

* fix: 약속회고 댓글/상세 조회 mock 처리 개선 (#96)

* refactor: 약속회고 댓글 삭제 확인 모달 개선 및 코드 정리 (#96)

- window.confirm을 글로벌 모달로 변경
- Mock 함수에서 불필요한 meetingId 파라미터 제거
- Textarea 컴포넌트에서 사용하지 않는 ref 제거.

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

* [feat] 페이지 접근 권한 전역 관리 구현 (#105)

* feat: 페이지 접근 권한 전역 관리 구현 (#99)

인터셉터에서 권한 에러 감지 시 커스텀 DOM 이벤트를 dispatch하고,
RootLayout의 usePermissionRedirect 훅에서 토스트와 홈 리다이렉트를
처리하여 한 곳에서 통합 관리.

- errors.ts: PAGE_ACCESS_ERROR_CODES Set 추가 (6개 에러 코드)
- interceptors.ts: permission-denied 커스텀 이벤트 dispatch
- usePermissionRedirect: 이벤트 → 토스트 + navigate(HOME)
- RootLayout: usePermissionRedirect 훅 연결
- MeetingRetrospectiveCreatePage: 이중 내비게이션 방지

* fix: 권한 에러 중복 이벤트 억제 및 핸들러 방어 코드 추가 (#99)

* fix: 권한 에러 시 중복 토스트/모달 방지 (#99)

* fix: 약속상세 페이지의 사전의견 버튼에 링크 연동(#106) (#107)

---------

Co-authored-by: choiyoungae <109134495+choiyoungae@users.noreply.github.com>
Co-authored-by: haeun <110523397+haruyam15@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 약속 회고 AI 요약 결과 UI 및 수정/발행 구현

2 participants