Skip to content

Comments

feat: 약속 회고 AI 요약 결과 UI 구현#85

Merged
mgYang53 merged 5 commits intodevelopfrom
feat/meeting-retro-summary-result-84
Feb 19, 2026
Merged

feat: 약속 회고 AI 요약 결과 UI 구현#85
mgYang53 merged 5 commits intodevelopfrom
feat/meeting-retro-summary-result-84

Conversation

@mgYang53
Copy link
Contributor

@mgYang53 mgYang53 commented Feb 17, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

약속 회고 생성 페이지에서 "AI 요약 시작하기" 클릭 시 결과 페이지로 이동하여
로딩 오버레이 → 스켈레톤 → 완료 토스트 순서로 AI 요약 플로우 UI를 표시합니다.
실제 API 연동은 백엔드 확정 후 교체 예정 (현재 setTimeout mock).

🔧 변경 사항

  • 약속 회고 생성/결과 페이지 및 라우트 추가
  • AI 로딩 오버레이(AiLoadingOverlay): 딤 배경 + 그래디언트 AI 아이콘 + 펄스 애니메이션 + 취소 버튼
  • AI 요약 완료 토스트(AiSummaryToast): 화면 중앙 페이드 인/아웃 + 자동 dismiss
  • 피그마 디자인 기반 그래디언트 AI 아이콘(AiGradientIcon) 컴포넌트
  • 회고 스켈레톤(RetrospectiveSummarySkeleton) 컴포넌트
  • sonner 토스트 라이브러리 및 Sonner 컴포넌트 연동
  • 약속 상세 → 회고 네비게이션 연결

📸 스크린샷 (선택 사항)

스크린샷 2026-02-17 오후 5 41 10 스크린샷 2026-02-17 오후 5 41 14 스크린샷 2026-02-17 오후 5 41 17

📄 기타

  • 임시로 틀만 잡아놓은 형태이며, 실제 api 연동은 백엔드 확인 후 진행 필요합니다.
  • @haruyam15 약속 회고 생성까지는 만들어놓은 페이지와 컴포넌트 이용해서 자유롭게 진행하시면 됩니다.

Summary by CodeRabbit

출시 노트

  • 새로운 기능
    • 회의 완료 후 회고 기능이 추가되었습니다
    • 회의 상세 페이지에서 회고 버튼으로 쉽게 접근 가능합니다
    • AI 요약 기능으로 회고 생성을 지원합니다
    • 사전 의견 수집 및 녹음 파일 업로드 기능이 포함됩니다
    • AI 처리 중 로딩 표시와 완료 알림으로 명확한 피드백을 제공합니다

약속 회고 생성 페이지에서 AI 요약 시작 시 결과 페이지로 이동하여
로딩 오버레이 → 스켈레톤 → 완료 토스트 플로우를 표시하는 UI 구현.
실제 API 연동은 백엔드 확정 후 교체 예정 (현재 setTimeout mock).

- 약속 회고 생성/결과 페이지 및 라우트 추가
- AI 로딩 오버레이(AiLoadingOverlay) + 완료 토스트(AiSummaryToast)
- 피그마 디자인 기반 그래디언트 AI 아이콘 및 펄스 애니메이션
- 회고 스켈레톤(RetrospectiveSummarySkeleton) 컴포넌트
- sonner 토스트 라이브러리 및 Sonner 컴포넌트 연동
- 약속 상세 → 회고 네비게이션 연결
@mgYang53 mgYang53 linked an issue Feb 17, 2026 that may be closed by this pull request
19 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Walkthrough

새로운 회고 기능을 위해 AI 요약 UI 컴포넌트(아이콘, 로딩 오버레이, 토스트), 회고 페이지 및 생성 페이지, 관련 라우팅을 추가하고 미팅 상세 페이지에서 진입점을 연결했습니다.

Changes

코호트 / 파일(들) 요약
UI 컴포넌트
src/features/retrospectives/components/AiGradientIcon.tsx, src/features/retrospectives/components/AiLoadingOverlay.tsx, src/features/retrospectives/components/AiSummaryToast.tsx, src/features/retrospectives/components/RetrospectiveCardButtons.tsx, src/features/retrospectives/components/RetrospectiveSummarySkeleton.tsx
AI 요약 관련 UI 컴포넌트들(그래디언트 아이콘, 로딩 오버레이, 완료 토스트, 회고 카드 버튼, 스켈레톤) 추가.
바렐 export
src/features/retrospectives/components/index.ts, src/features/retrospectives/index.ts, src/pages/Retrospectives/index.ts, src/pages/index.ts
회고 관련 컴포넌트 및 페이지들을 공개 export로 통합.
회고 페이지
src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx, src/pages/Retrospectives/MeetingRetrospectivePage.tsx
회고 생성 및 조회 페이지 추가. AI 요약 상태 흐름 포함.
라우팅 및 상수
src/routes/index.tsx, src/shared/constants/routes.ts
회고 관련 라우트 2개 추가(/retrospective, /retrospective/create).
기존 페이지 통합
src/pages/Meetings/MeetingDetailPage.tsx
POST 상태일 때 RetrospectiveCardButtons 렌더링 추가.
UI 조정
src/shared/ui/Sonner.tsx
에러 토스트 아이콘을 space에서 빈 fragment로 변경.

Sequence Diagram

sequenceDiagram
    participant User
    participant MeetingDetailPage
    participant MeetingRetrospectivePage
    participant MeetingRetrospectiveCreatePage
    participant AiLoadingOverlay
    participant AiSummaryToast

    User->>MeetingDetailPage: 미팅 상세 페이지 진입 (POST 상태)
    MeetingDetailPage->>User: RetrospectiveCardButtons 표시
    User->>MeetingRetrospectiveCreatePage: "약속 회고" 클릭
    MeetingRetrospectiveCreatePage->>MeetingRetrospectivePage: AI 요약 시작 (fromAiSummary=true)
    MeetingRetrospectivePage->>AiLoadingOverlay: AI 요약 중 오버레이 표시
    Note over AiLoadingOverlay: 3초 대기
    AiLoadingOverlay->>AiLoadingOverlay: 로딩 완료
    MeetingRetrospectivePage->>AiSummaryToast: 완료 토스트 표시
    AiSummaryToast->>User: 완료 메시지 (3초 표시 후 사라짐)
Loading

추정 리뷰 노력

🎯 3 (중간) | ⏱️ ~20분

이유: 새로운 컴포넌트/페이지 5개 추가로 분산된 변경이 많지만, 각각 단순하고 독립적 구조입니다. AI 요약 흐름에서 상태 관리 로직(타이머, 모달 제어)이 있어 검토 주의 필요합니다.

가능한 관련 이슈

가능한 관련 PR

제안 리뷰어

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 제목은 주요 변경사항(AI 요약 결과 UI 구현)을 명확하게 나타내며, 변경 내용과 일치합니다.

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

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

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.

@mgYang53 mgYang53 changed the title [feat] 약속 회고 AI 요약 결과 UI 구현 (#84) feat: 약속 회고 AI 요약 결과 UI 구현 Feb 17, 2026
@mgYang53 mgYang53 self-assigned this Feb 17, 2026
@mgYang53 mgYang53 added the feat 새로운 기능 추가 label Feb 17, 2026
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 (6)
src/features/retrospectives/components/AiGradientIcon.tsx (1)

9-19: 하드코딩된 gradient id="ai-gradient" — 충돌 가능성 낮지만 참고.

동일 페이지에 다른 SVG가 같은 id를 사용하면 그라디언트가 예기치 않게 덮어씌워질 수 있습니다. 현재 사용 범위에서는 문제없지만, 재사용 범위가 넓어지면 useId()로 고유 id 생성을 고려해주세요.

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

In `@src/features/retrospectives/components/AiGradientIcon.tsx` around lines 9 -
19, The hardcoded gradient id "ai-gradient" can collide with other SVGs; in the
AiGradientIcon component generate a unique id using React's useId() (import
useId from 'react'), e.g. const gradientId = `${useId()}-ai-gradient`, then
replace the linearGradient id and any fill/url references (e.g.
fill="url(`#ai-gradient`)") to use this gradientId so the gradient is unique
per-instance and avoids cross-SVG collisions.
src/shared/ui/Sonner.tsx (1)

10-10: icons: { error: ' ' } — 공백 문자열로 아이콘 숨김.

에러 아이콘을 공백 문자열로 대체하면 DOM에 빈 텍스트 노드가 남습니다. false 또는 빈 fragment를 사용하면 더 깔끔합니다.

♻️ 제안
-      icons={{ error: ' ' }}
+      icons={{ error: <></> }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Sonner.tsx` at line 10, Replace the placeholder space string
used to hide the error icon (icons: { error: ' ' }) in Sonner.tsx with a cleaner
falsy/iconless value; update the icons prop (icons: { error: false } or icons: {
error: () => null } or an empty fragment) so the DOM does not render an empty
text node — locate the icons prop usage in Sonner.tsx and swap the ' ' value for
false or a null-returning component.
src/features/retrospectives/components/AiSummaryToast.tsx (1)

1-67: Sonner 토스트 라이브러리와 커스텀 토스트 컴포넌트가 공존.

이 PR에서 Sonner(src/shared/lib/toast.ts, src/shared/ui/Sonner.tsx)를 전역 토스트로 도입하면서, 별도의 포탈 기반 커스텀 토스트(AiSummaryToast)도 추가했습니다. 디자인 요구사항(중앙 배치, 체크 아이콘 등)이 Sonner의 커스터마이징 범위를 벗어나는 것이라면 괜찮지만, 향후 토스트 관리 포인트가 이원화될 수 있으니 팀 내 합의를 확인해주세요.

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

In `@src/features/retrospectives/components/AiSummaryToast.tsx` around lines 1 -
67, There are now two toast systems (the custom AiSummaryToast component and the
Sonner-based global toast via toast/Sonner) which will split toast management;
pick one and consolidate: either adapt Sonner (Sonner component and exported
toast API) to support the centered check-icon design and durations used by
AiSummaryToast, then replace usages of AiSummaryToast with toast calls (ensuring
you map duration and onDismiss behavior), or migrate AiSummaryToast to use the
Sonner API internally (wrap Sonner/toast to render the centered portal design)
and remove the standalone portal usage; update all places that import
AiSummaryToast or call the global toast to use the chosen unified API and ensure
dismissal, timing (duration), and accessibility are preserved.
src/pages/Retrospectives/MeetingRetrospectivePage.tsx (2)

16-21: useParams non-null assertion — 첫 번째 파일과 동일한 문제

gatheringId!, meetingId!가 Line 50, 62에서 사용됩니다. 위 파일과 동일하게 early return guard를 권장합니다.

추가로 Line 21의 location.state as { fromAiSummary?: boolean }는 동작하지만, 향후 state 필드가 늘어날 경우 별도 타입을 정의하면 유지보수가 편해집니다.

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

In `@src/pages/Retrospectives/MeetingRetrospectivePage.tsx` around lines 16 - 21,
The component reads gatheringId and meetingId from useParams and later uses
non-null assertions (gatheringId!, meetingId!), so add an early-return guard at
the start of MeetingRetrospectivePage that checks both params and returns a
fallback (null or an error/redirect UI) when they are missing; replace the
non-null assertions with the guarded variables. Also replace the inline cast for
location.state with a dedicated type (e.g., MeetingRetrospectiveLocationState)
and use that type for the location/state access (and default fromAiSummary to
false), so future additions to state remain type-safe; update references like
gatheringId, meetingId, and fromAiSummary accordingly.

79-79: AiLoadingOverlayonCancel 미전달 — 사용자가 AI 요약을 취소할 수 없음

AiLoadingOverlay 컴포넌트는 onCancel prop이 있을 때 취소 버튼을 렌더링합니다(src/features/retrospectives/components/AiLoadingOverlay.tsx Line 26 참조). 현재 onCancel을 넘기지 않아 3초 동안 사용자가 아무 조작도 할 수 없습니다.

실제 API 연동 시 요청 시간이 길어질 수 있으므로, 취소 핸들러를 전달하는 것을 고려해 주세요.

💡 제안
-<AiLoadingOverlay isOpen={showOverlay} />
+<AiLoadingOverlay
+  isOpen={showOverlay}
+  onCancel={() => {
+    setShowOverlay(false)
+    setIsLoading(false)
+  }}
+/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Retrospectives/MeetingRetrospectivePage.tsx` at line 79,
AiLoadingOverlay is rendered without an onCancel handler so users cannot cancel
the AI summary (the component renders a cancel button only when onCancel is
provided). Fix by wiring an onCancel callback into the AiLoadingOverlay instance
(e.g., pass onCancel={handleCancelAiSummary}) that (1) updates local state to
hide the overlay (call setShowOverlay(false) or equivalent) and (2) aborts the
in-flight AI request if applicable (use your AbortController or cancellation
mechanism used by the API call). Ensure handleCancelAiSummary is defined near
the summary-starting logic and referenced here so the overlay can be dismissed
immediately.
src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx (1)

9-18: useParams 값에 non-null assertion(!) 사용 — undefined 가능성 존재

useParamsstring | undefined를 반환합니다. URL이 예상과 다르게 매칭되면 gatheringId!, meetingId!undefined인 채로 ROUTES.MEETING_RETROSPECTIVE(undefined, undefined)가 호출되어 잘못된 경로(/gatherings/undefined/meetings/undefined/retrospective)로 이동합니다.

early return guard를 추가하거나, 값이 없을 때 에러 UI를 보여주는 방식을 권장합니다.

🛡️ 제안
 const { gatheringId, meetingId } = useParams<{
   gatheringId: string
   meetingId: string
 }>()

+if (!gatheringId || !meetingId) {
+  return <Navigate to={ROUTES.HOME} replace />
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx` around lines 9 -
18, The use of non-null assertions on useParams (gatheringId!, meetingId!) is
unsafe; update MeetingRetrospectiveCreatePage to guard against undefined params
by checking gatheringId and meetingId after calling useParams and rendering an
early return or error/placeholder UI when either is missing, and only call
ROUTES.MEETING_RETROSPECTIVE(gatheringId, meetingId) and render SubPageHeader
when both values are present; reference the useParams call, the gatheringId and
meetingId variables, the SubPageHeader component, and
ROUTES.MEETING_RETROSPECTIVE to locate and patch the code.
🤖 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/AiLoadingOverlay.tsx`:
- Line 3: Replace the relative import of Button in AiLoadingOverlay.tsx with the
project alias: update the import statement that currently references
'../../../shared/ui/Button' to use the alias '@/shared/ui/Button' so it matches
other files; ensure the change is applied to the import at the top of the
AiLoadingOverlay component where Button is imported.

In `@src/features/retrospectives/components/AiSummaryToast.tsx`:
- Around line 46-47: The effect in AiSummaryToast.tsx that depends on
[isVisible, duration] omits onDismiss, causing a stale-closure bug; either
stabilize the onDismiss prop with useCallback in the parent or inside
AiSummaryToast wrap it with a ref (latest pattern) and call ref.current in the
useEffect/timeout, or include onDismiss in the dependency array of the useEffect
so the timeout clears/restarts when onDismiss changes; update the component to
reference the stabilized onDismiss (useCallback or useRef) instead of relying on
an eslint-disable comment.
- Around line 37-45: The nested setTimeout(onDismiss, 300) inside the
dismissTimer closure isn’t cleared on unmount; update AiSummaryToast to track
that second timeout with a ref (e.g., create a dismissCallbackRef via useRef)
and store the inner timeout id there, add useRef to imports, and
clearTimeout(dismissCallbackRef.current) in the cleanup along with
cancelAnimationFrame(fadeInTimer) and clearTimeout(dismissTimer) so the
onDismiss callback can never run after unmount.

In `@src/features/retrospectives/components/RetrospectiveCardButtons.tsx`:
- Around line 1-55: The file's formatting in component RetrospectiveCardButtons
(export default function RetrospectiveCardButtons) is causing CI Prettier
warnings; run prettier --write against this file or the repo (e.g., prettier
--write src/features/retrospectives/components/RetrospectiveCardButtons.tsx),
stage the updated file, and commit the changes so the Prettier formatting issues
are resolved before merging.

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx`:
- Line 22: Prettier formatting warning: run the formatter (e.g., run `prettier
--write` or your project’s format script) and re-commit the changes to fix the
formatting on the JSX line that defines the sticky header (the div with
className "sticky top-[calc(var(--gnb-height)+59px)] z-30 flex items-center
justify-between bg-white pb-small" in MeetingRetrospectiveCreatePage.tsx);
ensure the file is saved and the updated formatted file is included in the PR.

---

Nitpick comments:
In `@src/features/retrospectives/components/AiGradientIcon.tsx`:
- Around line 9-19: The hardcoded gradient id "ai-gradient" can collide with
other SVGs; in the AiGradientIcon component generate a unique id using React's
useId() (import useId from 'react'), e.g. const gradientId =
`${useId()}-ai-gradient`, then replace the linearGradient id and any fill/url
references (e.g. fill="url(`#ai-gradient`)") to use this gradientId so the
gradient is unique per-instance and avoids cross-SVG collisions.

In `@src/features/retrospectives/components/AiSummaryToast.tsx`:
- Around line 1-67: There are now two toast systems (the custom AiSummaryToast
component and the Sonner-based global toast via toast/Sonner) which will split
toast management; pick one and consolidate: either adapt Sonner (Sonner
component and exported toast API) to support the centered check-icon design and
durations used by AiSummaryToast, then replace usages of AiSummaryToast with
toast calls (ensuring you map duration and onDismiss behavior), or migrate
AiSummaryToast to use the Sonner API internally (wrap Sonner/toast to render the
centered portal design) and remove the standalone portal usage; update all
places that import AiSummaryToast or call the global toast to use the chosen
unified API and ensure dismissal, timing (duration), and accessibility are
preserved.

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx`:
- Around line 9-18: The use of non-null assertions on useParams (gatheringId!,
meetingId!) is unsafe; update MeetingRetrospectiveCreatePage to guard against
undefined params by checking gatheringId and meetingId after calling useParams
and rendering an early return or error/placeholder UI when either is missing,
and only call ROUTES.MEETING_RETROSPECTIVE(gatheringId, meetingId) and render
SubPageHeader when both values are present; reference the useParams call, the
gatheringId and meetingId variables, the SubPageHeader component, and
ROUTES.MEETING_RETROSPECTIVE to locate and patch the code.

In `@src/pages/Retrospectives/MeetingRetrospectivePage.tsx`:
- Around line 16-21: The component reads gatheringId and meetingId from
useParams and later uses non-null assertions (gatheringId!, meetingId!), so add
an early-return guard at the start of MeetingRetrospectivePage that checks both
params and returns a fallback (null or an error/redirect UI) when they are
missing; replace the non-null assertions with the guarded variables. Also
replace the inline cast for location.state with a dedicated type (e.g.,
MeetingRetrospectiveLocationState) and use that type for the location/state
access (and default fromAiSummary to false), so future additions to state remain
type-safe; update references like gatheringId, meetingId, and fromAiSummary
accordingly.
- Line 79: AiLoadingOverlay is rendered without an onCancel handler so users
cannot cancel the AI summary (the component renders a cancel button only when
onCancel is provided). Fix by wiring an onCancel callback into the
AiLoadingOverlay instance (e.g., pass onCancel={handleCancelAiSummary}) that (1)
updates local state to hide the overlay (call setShowOverlay(false) or
equivalent) and (2) aborts the in-flight AI request if applicable (use your
AbortController or cancellation mechanism used by the API call). Ensure
handleCancelAiSummary is defined near the summary-starting logic and referenced
here so the overlay can be dismissed immediately.

In `@src/shared/ui/Sonner.tsx`:
- Line 10: Replace the placeholder space string used to hide the error icon
(icons: { error: ' ' }) in Sonner.tsx with a cleaner falsy/iconless value;
update the icons prop (icons: { error: false } or icons: { error: () => null }
or an empty fragment) so the DOM does not render an empty text node — locate the
icons prop usage in Sonner.tsx and swap the ' ' value for false or a
null-returning component.

mgYang53 and others added 3 commits February 17, 2026 17:55
- AiLoadingOverlay: 상대 경로 → @/ 별칭 import 수정
- AiSummaryToast: 내부 setTimeout 미정리 수정 (useRef로 cleanup)
- AiSummaryToast: onDismiss stale closure 수정 (useRef 패턴)
- AiGradientIcon: useId()로 gradient id 유니크 생성
- CreatePage/Page: useParams guard, non-null assertion 제거
- Page: location.state 타입 정의, onCancel 핸들러 연결
- Sonner: error icon 빈 문자열을 빈 Fragment로 교체
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.

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

21-21: 59px 하드코딩 — SubPageHeader 높이 변경 시 오정렬 위험

SubPageHeader 높이가 외부에서 CSS 변수로 노출되지 않아 59px를 직접 기입했지만, 추후 헤더 패딩·폰트 크기 조정 시 sticky 위치가 어긋날 수 있습니다.

♻️ 개선 제안: CSS 변수로 분리

globals.css 등에서 SubPageHeader 높이를 변수화하거나, 페이지 전반에서 동일한 오프셋을 쓰는 경우에는 공유 토큰으로 추출하는 것이 유지보수에 유리합니다.

-      <div className="sticky top-[calc(var(--gnb-height)+59px)] z-30 flex items-center justify-between bg-white pb-small">
+      <div className="sticky top-[calc(var(--gnb-height)+var(--sub-header-height))] z-30 flex items-center justify-between bg-white pb-small">

globals.css:

:root {
+  --sub-header-height: 59px; /* SubPageHeader 실측 높이 */
}

단기적으로 즉시 수정이 어렵다면 인라인 주석으로라도 의도를 명시해두는 것을 권장합니다.

-      <div className="sticky top-[calc(var(--gnb-height)+59px)] z-30 ...">
+      {/* 59px = SubPageHeader (py-small × 2 + medium TextButton 높이) */}
+      <div className="sticky top-[calc(var(--gnb-height)+59px)] z-30 ...">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx` at line 21,
Replace the hard-coded "59px" offset in the sticky container (the div with
className "sticky top-[calc(var(--gnb-height)+59px)] z-30 flex items-center
justify-between bg-white pb-small") with a CSS variable (e.g.
var(--sub-header-height)); add that variable to your global stylesheet
(globals.css or shared token) with the measured SubPageHeader height, or if you
cannot change globals immediately, add an inline comment next to the top calc
noting that 59px equals SubPageHeader height and TODO to replace with
--sub-header-height so future header height changes won’t break the sticky
offset.
src/pages/Retrospectives/MeetingRetrospectivePage.tsx (1)

65-65: 59px 매직 넘버 — CSS 변수 또는 디자인 토큰 사용 고려

문제: 59pxSubPageHeader의 높이를 하드코딩한 값입니다.

영향: SubPageHeader의 레이아웃(패딩, 폰트 크기 등)이 변경되면 이 계산식이 조용히 깨집니다.

대안: CSS 변수를 정의하거나 Tailwind 디자인 토큰으로 추출하는 것이 유지보수에 유리합니다.

♻️ CSS 변수 활용 예시

globals.css 등에 변수를 추가한 뒤:

/* e.g., in globals.css */
:root {
  --sub-page-header-height: 59px;
}

사용:

- className="sticky top-[calc(var(--gnb-height)+59px)] z-30 ..."
+ className="sticky top-[calc(var(--gnb-height)+var(--sub-page-header-height))] z-30 ..."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Retrospectives/MeetingRetrospectivePage.tsx` at line 65, Replace
the hardcoded 59px magic number used in the sticky top calculation in the div
with class "sticky top-[calc(var(--gnb-height)+59px)]" by introducing and using
a CSS variable or Tailwind design token (e.g., --sub-page-header-height) so the
layout ties to SubPageHeader's actual height; update the global CSS (or design
token config) to define the variable and change the top calc to use
var(--sub-page-header-height) in MeetingRetrospectivePage.tsx so future header
height changes remain consistent with the sticky offset.
src/features/retrospectives/components/AiSummaryToast.tsx (1)

52-56: cn 헬퍼로 통일 (선택 사항)

배열 .join(' ') 방식은 동작하지만, 코드베이스의 다른 컴포넌트들이 cn을 사용하는 패턴과 불일치합니다.

♻️ `cn` 헬퍼로 교체 제안
+import { cn } from '@/shared/lib/utils'
 ...
   <div
-    className={[
-      'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
-      'transition-opacity duration-300',
-      opacity ? 'opacity-100' : 'opacity-0',
-    ].join(' ')}
+    className={cn(
+      'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
+      'transition-opacity duration-300',
+      opacity ? 'opacity-100' : 'opacity-0',
+    )}
   >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/retrospectives/components/AiSummaryToast.tsx` around lines 52 -
56, Replace the array.join(' ') className construction in the AiSummaryToast
component with the shared cn helper to match project patterns; locate the
className prop in AiSummaryToast.tsx (the JSX that builds 'fixed left-1/2
top-1/2 z-50 ... transition-opacity ... opacity-100/opacity-0') and wrap those
class strings/conditional expression with cn(...) instead of using [.join(' ')],
preserving the conditional opacity logic and imports (add or reuse the cn import
if not already present).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/features/retrospectives/components/AiSummaryToast.tsx`:
- Around line 52-56: Replace the array.join(' ') className construction in the
AiSummaryToast component with the shared cn helper to match project patterns;
locate the className prop in AiSummaryToast.tsx (the JSX that builds 'fixed
left-1/2 top-1/2 z-50 ... transition-opacity ... opacity-100/opacity-0') and
wrap those class strings/conditional expression with cn(...) instead of using
[.join(' ')], preserving the conditional opacity logic and imports (add or reuse
the cn import if not already present).

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx`:
- Line 21: Replace the hard-coded "59px" offset in the sticky container (the div
with className "sticky top-[calc(var(--gnb-height)+59px)] z-30 flex items-center
justify-between bg-white pb-small") with a CSS variable (e.g.
var(--sub-header-height)); add that variable to your global stylesheet
(globals.css or shared token) with the measured SubPageHeader height, or if you
cannot change globals immediately, add an inline comment next to the top calc
noting that 59px equals SubPageHeader height and TODO to replace with
--sub-header-height so future header height changes won’t break the sticky
offset.

In `@src/pages/Retrospectives/MeetingRetrospectivePage.tsx`:
- Line 65: Replace the hardcoded 59px magic number used in the sticky top
calculation in the div with class "sticky top-[calc(var(--gnb-height)+59px)]" by
introducing and using a CSS variable or Tailwind design token (e.g.,
--sub-page-header-height) so the layout ties to SubPageHeader's actual height;
update the global CSS (or design token config) to define the variable and change
the top calc to use var(--sub-page-header-height) in
MeetingRetrospectivePage.tsx so future header height changes remain consistent
with the sticky offset.

@mgYang53 mgYang53 merged commit 24d8476 into develop Feb 19, 2026
2 checks passed
@mgYang53 mgYang53 deleted the feat/meeting-retro-summary-result-84 branch February 19, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] AI 요약 플로우 UI 구현 (로딩 오버레이 + 스켈레톤 + 토스트)

2 participants