Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough개인 회고(Personal Retrospective) 기능을 신규 추가했습니다. 라우트·페이지·컴포넌트·훅·API·타입·목데이터 등 작성·조회·수정·삭제 흐름 전반이 구현되어 네비게이션과 양식 제출/조회가 동작합니다. Changes
Sequence DiagramsequenceDiagram
actor User
participant Page as PersonalRetrospectivePage
participant FormHook as usePersonalRetrospectiveForm
participant APIHook as usePersonalRetrospective / usePersonalRetrospectiveEditForm
participant Mutation as useSave/UpdatePersonalRetrospective
participant Server as API_Server
User->>Page: 접근 (mode=create|edit)
Page->>APIHook: 초기 데이터 요청
APIHook->>Server: GET /personal-retrospective/detail 또는 /edit-form
Server-->>APIHook: 회고 데이터 응답
APIHook-->>Page: 데이터 전달
Page->>FormHook: 폼 초기화 (editFormData 있으면 전달)
FormHook->>FormHook: 내부 섹션 훅 초기화(Changed/Others/Free)
FormHook-->>Page: 폼 상태 제공
User->>Page: 입력/섹션 추가/삭제
Page->>FormHook: updateField / showSection / hideSection
FormHook-->>Page: 상태 갱신
User->>Page: 제출 클릭
Page->>FormHook: submit()
FormHook->>FormHook: 유효성 검사
alt 검증 실패
FormHook-->>Page: showErrors=true, scroll to error
else 검증 통과
FormHook->>Mutation: save/update 호출
Mutation->>Server: POST/PUT /personal-retrospective
Server-->>Mutation: 성공
Mutation-->>FormHook: onSuccess
FormHook-->>Page: 성공 콜백 (toast + navigate)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (14)
src/features/retrospectives/hooks/useOthersPerspective.ts (2)
91-93:reinit파라미터명items가 외부 상태items를 가립니다.같은 스코프에
const [items, setItems]가 있어 파라미터명 충돌로 가독성이 떨어집니다.♻️ 제안 수정
-const reinit = useCallback((items: OthersPerspectiveFormItem[]) => { - setItems(items) +const reinit = useCallback((newItems: OthersPerspectiveFormItem[]) => { + setItems(newItems) }, [])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useOthersPerspective.ts` around lines 91 - 93, The reinit callback's parameter name items shadows the component state const [items, setItems], hurting readability; rename the parameter (e.g., newItems or nextItems) in the useCallback definition for reinit and update any internal references so it calls setItems(newName) accordingly; ensure the exported/used symbol reinit and its call sites (if any) are updated to the new parameter name to avoid confusion.
54-85:isItemPartial로직이hasPartialInput에 그대로 중복되어 있습니다.두 곳에서
hasAny && !hasAll판별 로직을 반복 작성하고 있고,hasPartialInput은useMemo없이 인라인 계산되어 매 렌더마다 재연산됩니다.isItemPartial을 순수 함수로 추출하여 두 곳에서 재사용하고useMemo로 메모이제이션하는 것을 권장합니다.♻️ 제안 수정
+function checkPartial(item: OthersPerspectiveFormItem): boolean { + const hasAny = + item.speakerMemberId !== null || + item.topicId !== null || + item.opinion.trim() !== '' || + item.impact.trim() !== '' + const hasAll = + item.speakerMemberId !== null && + item.topicId !== null && + item.opinion.trim() !== '' && + item.impact.trim() !== '' + return hasAny && !hasAll +} const isItemPartial = useCallback( (id: string) => { const item = items.find((i) => i.id === id) if (!item) return false - const hasAny = ... - const hasAll = ... - return hasAny && !hasAll + return checkPartial(item) }, [items] ) -const hasPartialInput = items.some((item) => { - const hasAny = ... - const hasAll = ... - return hasAny && !hasAll -}) +const hasPartialInput = useMemo(() => items.some(checkPartial), [items])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useOthersPerspective.ts` around lines 54 - 85, Extract the duplicated partial-check logic into a pure helper (e.g., function itemIsPartial(item) { ... } that computes hasAny/hasAll using item.speakerMemberId, item.topicId, item.opinion.trim(), item.impact.trim()), update isItemPartial to use that helper (find the item by id then call itemIsPartial) and replace the inline hasPartialInput calculation with a memoized value: const hasPartialInput = useMemo(() => items.some(itemIsPartial), [items]); ensure all uses call the shared helper so the logic is not duplicated and isPartial computation is memoized.src/features/retrospectives/hooks/useFreeRecord.ts (2)
60-62:reinit파라미터명entries가 외부 상태entries를 가립니다.
useOthersPerspective의reinit과 동일한 패턴입니다.♻️ 제안 수정
-const reinit = useCallback((entries: FreeRecordEntryFormItem[]) => { - setEntries(entries) +const reinit = useCallback((newEntries: FreeRecordEntryFormItem[]) => { + setEntries(newEntries) }, [])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useFreeRecord.ts` around lines 60 - 62, The parameter name entries in the useCallback reinit shadows the outer state variable entries; rename the parameter (e.g., newEntries or nextEntries) in the reinit function declared in useFreeRecord.ts and update its internal setEntries call to use the new parameter name so the outer entries state is not hidden.
43-54:isEntryPartial로직이hasPartialInput에 중복되며,hasPartialInput은 메모이제이션 없이 매 렌더마다 재계산됩니다.
useOthersPerspective와 동일한 패턴입니다. 판별 로직을 한 곳으로 추출하고useMemo를 적용하세요.♻️ 제안 수정
+function isPartial(e: FreeRecordEntryFormItem): boolean { + return (e.title.trim() !== '') !== (e.content.trim() !== '') +} const isEntryPartial = useCallback( (id: string) => { const entry = entries.find((e) => e.id === id) if (!entry) return false - const hasTitle = entry.title.trim() !== '' - const hasContent = entry.content.trim() !== '' - return hasTitle !== hasContent + return isPartial(entry) }, [entries] ) -const hasPartialInput = entries.some((e) => (e.title.trim() !== '') !== (e.content.trim() !== '')) +const hasPartialInput = useMemo(() => entries.some(isPartial), [entries])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useFreeRecord.ts` around lines 43 - 54, The isEntryPartial logic is duplicated by hasPartialInput and hasPartialInput is recomputed each render; extract the shared predicate into a single helper (e.g., isEntryPartialPredicate) and replace the useCallback isEntryPartial and the hasPartialInput computation to reuse that helper, then memoize the boolean hasPartialInput with useMemo based on entries; update references to isEntryPartial (the useCallback) to call the shared predicate and keep the dependency arrays minimal (entries) similar to the pattern used in useOthersPerspective.src/features/retrospectives/hooks/useChangedThoughts.ts (2)
58-58:hasPartialInput = false하드코딩 — 의도를 명시해 주세요."바뀐 나의 생각"은 모든 필드가 선택 항목이라 partial 검증이 불필요한 설계인 것으로 보이지만, 주석 없이
false만 있으면 향후 버그로 오해받을 수 있습니다.♻️ 제안 수정
-const hasPartialInput = false +// 바뀐 나의 생각은 모든 필드가 선택 항목이므로 partial 검증이 필요 없습니다. +const hasPartialInput = false🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useChangedThoughts.ts` at line 58, The variable hasPartialInput is hardcoded to false which is ambiguous; either compute it from the thought DTO's optional fields (e.g., derive from title/description/tags/other selectable fields inside useChangedThoughts) so it accurately reflects whether any optional inputs are present, or replace the hardcoded value with a clear comment explaining the intentional always-false design; update the symbol hasPartialInput in useChangedThoughts.ts accordingly and ensure the computed expression or comment references the specific fields (title, description, tags or other optional fields) used to determine partial input.
52-56:formValues가 매 렌더마다 새 배열 참조를 생성합니다.
topics와overrides가 변하지 않아도 렌더마다 새 배열이 만들어져,formValues를 의존성으로 쓰는useEffect나React.memo컴포넌트의 불필요한 재실행을 유발할 수 있습니다.♻️ 제안 수정
-const formValues: ChangedThoughtFormItem[] = topics.map((t) => ({ - topicId: t.topicId, - coreSummary: overrides[t.topicId]?.coreSummary ?? '', - postOpinion: overrides[t.topicId]?.postOpinion ?? '', -})) +const formValues: ChangedThoughtFormItem[] = useMemo( + () => + topics.map((t) => ({ + topicId: t.topicId, + coreSummary: overrides[t.topicId]?.coreSummary ?? '', + postOpinion: overrides[t.topicId]?.postOpinion ?? '', + })), + [topics, overrides] +)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useChangedThoughts.ts` around lines 52 - 56, The current formValues constant is re-created on every render causing unnecessary downstream effects; wrap the computation in a useMemo to return the same array reference unless inputs change (use the existing symbols topics and overrides and the formValues consumer hooks/components such as any useEffect or React.memo that depend on formValues) so that formValues only recalculates when topics or overrides actually change.src/features/retrospectives/hooks/useDeletePersonalRetrospective.ts (1)
31-34: 삭제 성공 시 전체 키 invalidate는 범위가 과합니다.문제:
all키를 무효화해 개인 회고 전 캐시를 전부 흔듭니다.
영향: 다른 meeting의 불필요한 refetch가 발생할 수 있습니다.
대안:onSuccess에서 받은meetingId기준으로view/detail/editForm만 선택적으로 invalidate해 주세요.🔧 제안 diff
- onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: personalRetrospectiveQueryKeys.all, - }) - }, + onSuccess: async (_, meetingId) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: personalRetrospectiveQueryKeys.view(meetingId), + }), + queryClient.invalidateQueries({ + queryKey: personalRetrospectiveQueryKeys.editForm(meetingId), + }), + ]) + },As per coding guidelines "queryKey 안정성, enabled 조건, select 비용, invalidate/refetch 타이밍을 중점적으로 봐줘."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useDeletePersonalRetrospective.ts` around lines 31 - 34, The onSuccess handler in useDeletePersonalRetrospective currently calls queryClient.invalidateQueries with personalRetrospectiveQueryKeys.all which is too broad; change it to only invalidate the specific query keys for the deleted meeting (e.g., personalRetrospectiveQueryKeys.view(meetingId), personalRetrospectiveQueryKeys.detail(meetingId), and personalRetrospectiveQueryKeys.editForm(meetingId)) using the meetingId available from the mutation variables or response (inside the onSuccess callback), rather than invalidating the global .all key, so only that meeting's view/detail/editForm cache entries are refetched.src/features/retrospectives/hooks/useSavePersonalRetrospective.ts (1)
25-28: 저장 성공 후 캐시 동기화 훅을 추가해 주세요.문제: 저장 mutation이 성공해도 관련 쿼리를 invalidate하지 않습니다.
영향: 같은 meeting 캐시가 이미 있으면 다음 화면에서 잠시 구값이 보일 수 있습니다.
대안:onSuccess에서view(meetingId)(필요시editForm) invalidate를 추가해 주세요.As per coding guidelines "queryKey 안정성, enabled 조건, select 비용, invalidate/refetch 타이밍을 중점적으로 봐줘."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useSavePersonalRetrospective.ts` around lines 25 - 28, Add an onSuccess handler to the useSavePersonalRetrospective mutation so that after savePersonalRetrospective succeeds you invalidate the relevant meeting queries to keep caches in sync; specifically, in useSavePersonalRetrospective (which returns useMutation<void, ApiError, SavePersonalRetrospectiveParams> with mutationFn: savePersonalRetrospective) call the react-query QueryClient to invalidate the view(meetingId) query (and editForm if applicable) using the meetingId from params in onSuccess, ensuring you use the same queryKey shape (e.g., ['view', meetingId] or the project's view(...) helper) and avoid unnecessary refetches by targeting only the affected keys.src/features/retrospectives/personalRetrospective.constants.ts (1)
7-8: sticky offset 매직넘버는 토큰 기반으로 맞추는 편이 안전합니다.문제: 오프셋 값이
220으로 하드코딩되어 있습니다.
영향: 헤더 높이 변경 시 앵커 스크롤 위치가 쉽게 어긋납니다.
대안: CSS 변수/공용 토큰과 단일 소스로 연결해 값 드리프트를 막아주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personalRetrospective.constants.ts` around lines 7 - 8, PERSONAL_RETRO_STICKY_OFFSET is a hardcoded magic number; replace it with a value derived from a single source-of-truth token or CSS variable instead of 220. Update the export to compute/derive the offset from your shared layout tokens (e.g., import header height tokens like GNB_HEIGHT, SUBPAGE_HEADER_HEIGHT, PAGE_HEADER_HEIGHT, PADDING from the design tokens module) or read a CSS custom property via getComputedStyle(document.documentElement).getPropertyValue('--your-sticky-offset') and parseInt it, and export that computed value so changes to tokens/CSS automatically update the offset.src/features/retrospectives/hooks/useUpdatePersonalRetrospective.ts (1)
32-35: 수정 성공 후 invalidate 범위를 meeting별로 조금 더 넓혀주세요.문제: 현재
view키만 무효화합니다.
영향: 재편집 시editForm캐시가 남아 최신값이 늦게 반영될 수 있습니다.
대안:view(meetingId)와 함께editForm(meetingId)도 같이 invalidate해 주세요.🔧 제안 diff
- onSuccess: (_, { meetingId }) => { - queryClient.invalidateQueries({ - queryKey: personalRetrospectiveQueryKeys.view(meetingId), - }) - }, + onSuccess: async (_, { meetingId }) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: personalRetrospectiveQueryKeys.view(meetingId), + }), + queryClient.invalidateQueries({ + queryKey: personalRetrospectiveQueryKeys.editForm(meetingId), + }), + ]) + },As per coding guidelines "queryKey 안정성, enabled 조건, select 비용, invalidate/refetch 타이밍을 중점적으로 봐줘."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/hooks/useUpdatePersonalRetrospective.ts` around lines 32 - 35, Update the onSuccess handler in useUpdatePersonalRetrospective's mutation so that queryClient.invalidateQueries invalidates both personalRetrospectiveQueryKeys.view(meetingId) and personalRetrospectiveQueryKeys.editForm(meetingId) (i.e., call invalidateQueries for both keys or pass an array), ensuring the editForm cache is cleared after a successful update to prevent stale edit-form state; keep the existing meetingId destructuring and preserve any other invalidate logic.src/features/retrospectives/personalRetrospective.mock.ts (2)
50-51: 인라인 base64 이미지 데이터는 목데이터라도 분리하는 게 안전합니다.문제: 대용량 base64 문자열을 코드에 직접 넣었습니다.
영향: 번들 크기와 파싱 비용이 커지고, PR/리뷰 가독성이 크게 떨어집니다.
대안: 짧은 placeholder URL 또는 별도 fixture 파일/상수로 분리해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personalRetrospective.mock.ts` around lines 50 - 51, The profileImage property currently embeds a huge base64 string inline (making the mock heavy and noisy); extract that large base64 payload into a separate fixture (e.g., export a PROFILE_IMAGE_BASE64 constant or place the data in a .json/fixture file) or replace it with a short placeholder URL/data URI, then update the mock to import/reference that constant instead of embedding the string directly (locate the profileImage field in the mock object and replace the inline value).
62-64: 목데이터 반환 시 동일 객체 레퍼런스 공유를 피해주세요.문제: getter가 내부 상수를 그대로 반환합니다.
영향: 호출부에서 값이 변경되면 다음 호출 결과까지 오염될 수 있습니다.
대안: 반환 시 복제본을 주거나, 호출부에서 불변 객체로만 다루도록 계약을 명확히 해주세요.Also applies to: 162-164, 229-232
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personalRetrospective.mock.ts` around lines 62 - 64, The getters currently return internal mock constants by reference (e.g., getMockPersonalRetrospectiveDetail returning mockPersonalRetrospectiveDetail), causing callers to accidentally share/mutate the same object; change each getter to return a deep clone instead (use structuredClone(mockPersonalRetrospectiveDetail) or JSON.parse(JSON.stringify(...)) / lodash.cloneDeep as fallback) and apply the same replacement to the other mock getters in this file that return internal constants so every call returns an isolated copy.src/features/retrospectives/components/ChangedThoughtsSection.tsx (1)
51-56: 토픽명 조회가 렌더마다 선형 탐색 반복입니다문제:
formValues.map내부에서topics.find를 매번 수행합니다.
영향: 항목이 많아지면 리렌더 비용이 불필요하게 커집니다.
대안:topicId -> topicName맵을useMemo로 만들어 O(1) 조회로 바꿔주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/components/ChangedThoughtsSection.tsx` around lines 51 - 56, Inside ChangedThoughtsSection.tsx the render loop uses topics.find(...) inside formValues.map, causing O(n) lookup per render; create a memoized map from topicId to topicName (e.g., via useMemo) using the topics array and then replace topics.find(...) with an O(1) lookup from that map (referencing formValues, topics.find, topicId and topicName) so each render uses the memoized map for fast lookups.src/pages/Retrospectives/PersonalRetrospectivePage.tsx (1)
51-57: 수정 모드 preOpinions 매핑에서 반복 탐색이 있습니다문제:
changedThoughts.map내부에서editFormData?.topics.find를 반복합니다.
영향: 토픽 수가 늘면 계산 비용이 증가합니다.
대안:topics를 미리 맵으로 변환해 재사용해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Retrospectives/PersonalRetrospectivePage.tsx` around lines 51 - 57, The current preOpinions mapping repeatedly calls editFormData?.topics.find inside the map which causes O(n*m) work; to fix, create a lookup (e.g., topicMap) from editFormData?.topics once (using Array.prototype.reduce or a Map keyed by topicId) and then in the preOpinions computation replace editFormData?.topics.find(...) with a direct lookup like topicMap[ct.topicId] or topicMap.get(ct.topicId); update references to preOpinions, isEditMode, and editFormData?.retrospective.changedThoughts to use that lookup so each changedThought does an O(1) lookup instead of a find.
🤖 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/FreeRecordSection.tsx`:
- Around line 59-61: In FreeRecordSection.tsx, the title field is showing the
wrong error text; when prop/variable titleError is true you should set the
errorMessage for that input to "제목을 입력해주세요" instead of "내용을 입력해주세요". Locate the
title input in the FreeRecordSection component (look for the error={titleError}
and errorMessage= usage) and replace the message string to "제목을 입력해주세요" so the
error matches the title field.
In `@src/features/retrospectives/components/OthersPerspectiveSection.tsx`:
- Around line 167-169: The Button in OthersPerspectiveSection currently omits an
explicit type, which inside a surrounding form can act as a submit button;
update the <Button> that calls addItem to include type="button" so it won't
trigger form submission (locate the Button element in OthersPerspectiveSection
that uses the addItem handler and add the type prop).
In `@src/features/retrospectives/components/PersonalRetrospectiveViewContent.tsx`:
- Around line 183-184: Change unstable index keys for the lists: for
othersPerspectives replace key={index} with a stable identifier such as
key={item.topicId} or a composite key like
`${item.topicId}-${item.meetingMemberId}` (refer to othersPerspectives and
item.topicId/item.meetingMemberId); for freeTexts avoid key={index} by having
the API provide a stable id field, or temporarily build a deterministic
composite key (e.g., `${freeText.title}-${hash(freeText.content)}`) so React can
reliably track items.
In `@src/features/retrospectives/hooks/usePersonalRetrospectiveForm.ts`:
- Around line 160-217: Add a guard at the very start of the submit function to
return immediately if a pending request is in-flight: check the mutation/loading
state used by save/update (e.g. savePending/saveIsLoading or
updatePending/updateIsLoading) and return early (or unify into a single
isSubmitting flag) before any validation or building of requestBody; place this
check above the hasPartial logic so submit won't call save/update again when
isEditMode is true and update is pending (or when not edit mode and save is
pending).
In `@src/features/retrospectives/hooks/usePersonalRetrospectiveView.ts`:
- Around line 30-35: The query currently sets gcTime but not staleTime, causing
immediate staleness and unwanted refetches; update the useQuery call in
usePersonalRetrospectiveView (the hook using personalRetrospectiveQueryKeys.view
and getPersonalRetrospectiveView) to include a staleTime (e.g., the same 10 * 60
* 1000 value used for gcTime or another appropriate duration less than or equal
to gcTime) so the retrospective view stays "fresh" and avoids background
refetches on remount/tab focus.
In `@src/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx`:
- Around line 22-31: Validate that parsed numeric IDs are finite positive
numbers before rendering or calling mutations/navigations: after computing
gatheringId = Number(gatheringIdParam) and meetingId = Number(meetingIdParam),
check Number.isFinite(gatheringId) && gatheringId > 0 and
Number.isFinite(meetingId) && meetingId > 0 (instead of only checking
gatheringIdParam/meetingIdParam); if validation fails, return early or call
navigate to a safe route; ensure any uses of gatheringId/meetingId (e.g., in
usePersonalRetrospectiveView, deleteRetrospective, and navigation calls) only
occur after this validation.
In `@src/shared/styles/theme.css`:
- Around line 230-238: The two `@utility` blocks sticky-below-subheader and
sticky-retro-view-nav only set top and omit position: sticky, and
sticky-retro-view-nav hardcodes 65px and 24px; update each utility to include
position: sticky and replace the hardcoded 65px and 24px with theme CSS
variables/tokens (e.g., --spacing-page-header-height and --spacing-retro-padding
or existing `@theme` tokens) so the utilities work standalone and remain
maintainable; alternatively, if you prefer not to expose sticky behavior there,
rename the utilities to remove the "sticky-" prefix (e.g., top-below-subheader)
and document that they must be used with the sticky class.
---
Nitpick comments:
In `@src/features/retrospectives/components/ChangedThoughtsSection.tsx`:
- Around line 51-56: Inside ChangedThoughtsSection.tsx the render loop uses
topics.find(...) inside formValues.map, causing O(n) lookup per render; create a
memoized map from topicId to topicName (e.g., via useMemo) using the topics
array and then replace topics.find(...) with an O(1) lookup from that map
(referencing formValues, topics.find, topicId and topicName) so each render uses
the memoized map for fast lookups.
In `@src/features/retrospectives/hooks/useChangedThoughts.ts`:
- Line 58: The variable hasPartialInput is hardcoded to false which is
ambiguous; either compute it from the thought DTO's optional fields (e.g.,
derive from title/description/tags/other selectable fields inside
useChangedThoughts) so it accurately reflects whether any optional inputs are
present, or replace the hardcoded value with a clear comment explaining the
intentional always-false design; update the symbol hasPartialInput in
useChangedThoughts.ts accordingly and ensure the computed expression or comment
references the specific fields (title, description, tags or other optional
fields) used to determine partial input.
- Around line 52-56: The current formValues constant is re-created on every
render causing unnecessary downstream effects; wrap the computation in a useMemo
to return the same array reference unless inputs change (use the existing
symbols topics and overrides and the formValues consumer hooks/components such
as any useEffect or React.memo that depend on formValues) so that formValues
only recalculates when topics or overrides actually change.
In `@src/features/retrospectives/hooks/useDeletePersonalRetrospective.ts`:
- Around line 31-34: The onSuccess handler in useDeletePersonalRetrospective
currently calls queryClient.invalidateQueries with
personalRetrospectiveQueryKeys.all which is too broad; change it to only
invalidate the specific query keys for the deleted meeting (e.g.,
personalRetrospectiveQueryKeys.view(meetingId),
personalRetrospectiveQueryKeys.detail(meetingId), and
personalRetrospectiveQueryKeys.editForm(meetingId)) using the meetingId
available from the mutation variables or response (inside the onSuccess
callback), rather than invalidating the global .all key, so only that meeting's
view/detail/editForm cache entries are refetched.
In `@src/features/retrospectives/hooks/useFreeRecord.ts`:
- Around line 60-62: The parameter name entries in the useCallback reinit
shadows the outer state variable entries; rename the parameter (e.g., newEntries
or nextEntries) in the reinit function declared in useFreeRecord.ts and update
its internal setEntries call to use the new parameter name so the outer entries
state is not hidden.
- Around line 43-54: The isEntryPartial logic is duplicated by hasPartialInput
and hasPartialInput is recomputed each render; extract the shared predicate into
a single helper (e.g., isEntryPartialPredicate) and replace the useCallback
isEntryPartial and the hasPartialInput computation to reuse that helper, then
memoize the boolean hasPartialInput with useMemo based on entries; update
references to isEntryPartial (the useCallback) to call the shared predicate and
keep the dependency arrays minimal (entries) similar to the pattern used in
useOthersPerspective.
In `@src/features/retrospectives/hooks/useOthersPerspective.ts`:
- Around line 91-93: The reinit callback's parameter name items shadows the
component state const [items, setItems], hurting readability; rename the
parameter (e.g., newItems or nextItems) in the useCallback definition for reinit
and update any internal references so it calls setItems(newName) accordingly;
ensure the exported/used symbol reinit and its call sites (if any) are updated
to the new parameter name to avoid confusion.
- Around line 54-85: Extract the duplicated partial-check logic into a pure
helper (e.g., function itemIsPartial(item) { ... } that computes hasAny/hasAll
using item.speakerMemberId, item.topicId, item.opinion.trim(),
item.impact.trim()), update isItemPartial to use that helper (find the item by
id then call itemIsPartial) and replace the inline hasPartialInput calculation
with a memoized value: const hasPartialInput = useMemo(() =>
items.some(itemIsPartial), [items]); ensure all uses call the shared helper so
the logic is not duplicated and isPartial computation is memoized.
In `@src/features/retrospectives/hooks/useSavePersonalRetrospective.ts`:
- Around line 25-28: Add an onSuccess handler to the
useSavePersonalRetrospective mutation so that after savePersonalRetrospective
succeeds you invalidate the relevant meeting queries to keep caches in sync;
specifically, in useSavePersonalRetrospective (which returns useMutation<void,
ApiError, SavePersonalRetrospectiveParams> with mutationFn:
savePersonalRetrospective) call the react-query QueryClient to invalidate the
view(meetingId) query (and editForm if applicable) using the meetingId from
params in onSuccess, ensuring you use the same queryKey shape (e.g., ['view',
meetingId] or the project's view(...) helper) and avoid unnecessary refetches by
targeting only the affected keys.
In `@src/features/retrospectives/hooks/useUpdatePersonalRetrospective.ts`:
- Around line 32-35: Update the onSuccess handler in
useUpdatePersonalRetrospective's mutation so that queryClient.invalidateQueries
invalidates both personalRetrospectiveQueryKeys.view(meetingId) and
personalRetrospectiveQueryKeys.editForm(meetingId) (i.e., call invalidateQueries
for both keys or pass an array), ensuring the editForm cache is cleared after a
successful update to prevent stale edit-form state; keep the existing meetingId
destructuring and preserve any other invalidate logic.
In `@src/features/retrospectives/personalRetrospective.constants.ts`:
- Around line 7-8: PERSONAL_RETRO_STICKY_OFFSET is a hardcoded magic number;
replace it with a value derived from a single source-of-truth token or CSS
variable instead of 220. Update the export to compute/derive the offset from
your shared layout tokens (e.g., import header height tokens like GNB_HEIGHT,
SUBPAGE_HEADER_HEIGHT, PAGE_HEADER_HEIGHT, PADDING from the design tokens
module) or read a CSS custom property via
getComputedStyle(document.documentElement).getPropertyValue('--your-sticky-offset')
and parseInt it, and export that computed value so changes to tokens/CSS
automatically update the offset.
In `@src/features/retrospectives/personalRetrospective.mock.ts`:
- Around line 50-51: The profileImage property currently embeds a huge base64
string inline (making the mock heavy and noisy); extract that large base64
payload into a separate fixture (e.g., export a PROFILE_IMAGE_BASE64 constant or
place the data in a .json/fixture file) or replace it with a short placeholder
URL/data URI, then update the mock to import/reference that constant instead of
embedding the string directly (locate the profileImage field in the mock object
and replace the inline value).
- Around line 62-64: The getters currently return internal mock constants by
reference (e.g., getMockPersonalRetrospectiveDetail returning
mockPersonalRetrospectiveDetail), causing callers to accidentally share/mutate
the same object; change each getter to return a deep clone instead (use
structuredClone(mockPersonalRetrospectiveDetail) or
JSON.parse(JSON.stringify(...)) / lodash.cloneDeep as fallback) and apply the
same replacement to the other mock getters in this file that return internal
constants so every call returns an isolated copy.
In `@src/pages/Retrospectives/PersonalRetrospectivePage.tsx`:
- Around line 51-57: The current preOpinions mapping repeatedly calls
editFormData?.topics.find inside the map which causes O(n*m) work; to fix,
create a lookup (e.g., topicMap) from editFormData?.topics once (using
Array.prototype.reduce or a Map keyed by topicId) and then in the preOpinions
computation replace editFormData?.topics.find(...) with a direct lookup like
topicMap[ct.topicId] or topicMap.get(ct.topicId); update references to
preOpinions, isEditMode, and editFormData?.retrospective.changedThoughts to use
that lookup so each changedThought does an O(1) lookup instead of a find.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (31)
src/features/retrospectives/components/ChangedThoughtsSection.tsxsrc/features/retrospectives/components/FreeRecordSection.tsxsrc/features/retrospectives/components/OthersPerspectiveSection.tsxsrc/features/retrospectives/components/PersonalRetrospectiveContent.tsxsrc/features/retrospectives/components/PersonalRetrospectiveViewContent.tsxsrc/features/retrospectives/components/RetrospectiveCardButtons.tsxsrc/features/retrospectives/components/index.tssrc/features/retrospectives/hooks/index.tssrc/features/retrospectives/hooks/personalRetrospectiveQueryKeys.tssrc/features/retrospectives/hooks/useChangedThoughts.tssrc/features/retrospectives/hooks/useDeletePersonalRetrospective.tssrc/features/retrospectives/hooks/useFreeRecord.tssrc/features/retrospectives/hooks/useOthersPerspective.tssrc/features/retrospectives/hooks/usePersonalRetrospective.tssrc/features/retrospectives/hooks/usePersonalRetrospectiveEditForm.tssrc/features/retrospectives/hooks/usePersonalRetrospectiveForm.tssrc/features/retrospectives/hooks/usePersonalRetrospectiveView.tssrc/features/retrospectives/hooks/useSavePersonalRetrospective.tssrc/features/retrospectives/hooks/useUpdatePersonalRetrospective.tssrc/features/retrospectives/index.tssrc/features/retrospectives/personalRetrospective.api.tssrc/features/retrospectives/personalRetrospective.constants.tssrc/features/retrospectives/personalRetrospective.endpoints.tssrc/features/retrospectives/personalRetrospective.mock.tssrc/features/retrospectives/personalRetrospective.types.tssrc/pages/Retrospectives/PersonalRetrospectivePage.tsxsrc/pages/Retrospectives/PersonalRetrospectiveViewPage.tsxsrc/pages/Retrospectives/index.tssrc/routes/index.tsxsrc/shared/constants/routes.tssrc/shared/styles/theme.css
src/features/retrospectives/components/OthersPerspectiveSection.tsx
Outdated
Show resolved
Hide resolved
src/features/retrospectives/components/PersonalRetrospectiveViewContent.tsx
Outdated
Show resolved
Hide resolved
src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts
Show resolved
Hide resolved
src/features/retrospectives/personal/hooks/usePersonalRetrospectiveView.ts
Show resolved
Hide resolved
… feat/personal-retro-90
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx (1)
22-36: NaN 검증 처리 확인 — LGTM이전 리뷰에서 지적된
Number.isFinite+> 0검증이 정상적으로 반영됐습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx` around lines 22 - 36, The numeric validation for route params has been correctly implemented: keep the Number.isFinite checks combined with > 0 for gatheringId and meetingId in PersonalRetrospectiveViewPage (variables gatheringId, meetingId) as-is; no further changes are required here.
🤖 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/PersonalRetrospectiveViewPage.tsx`:
- Around line 77-79: The separator "·" is rendered while data is undefined;
update the JSX in PersonalRetrospectiveViewPage.tsx so the whole line uses
conditional rendering: only render the paragraph (or build the string) when data
and/or data.meetingHeaderInfo exist, and when concatenating use checks for
meetingHeaderInfo.bookTitle and bookAuthor so the "·" is included only if both
values are present (refer to data, meetingHeaderInfo.bookTitle and
meetingHeaderInfo.bookAuthor to locate and fix the code).
---
Duplicate comments:
In `@src/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx`:
- Around line 22-36: The numeric validation for route params has been correctly
implemented: keep the Number.isFinite checks combined with > 0 for gatheringId
and meetingId in PersonalRetrospectiveViewPage (variables gatheringId,
meetingId) as-is; no further changes are required here.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/features/retrospectives/components/OthersPerspectiveSection.tsxsrc/features/retrospectives/components/PersonalRetrospectiveViewContent.tsxsrc/features/retrospectives/hooks/index.tssrc/features/retrospectives/hooks/usePersonalRetrospectiveForm.tssrc/features/retrospectives/hooks/usePersonalRetrospectiveView.tssrc/features/retrospectives/index.tssrc/features/retrospectives/personalRetrospective.constants.tssrc/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
- src/features/retrospectives/personalRetrospective.constants.ts
- src/features/retrospectives/hooks/usePersonalRetrospectiveForm.ts
- src/features/retrospectives/index.ts
- src/features/retrospectives/components/OthersPerspectiveSection.tsx
- src/features/retrospectives/hooks/usePersonalRetrospectiveView.ts
- src/features/retrospectives/components/PersonalRetrospectiveViewContent.tsx
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx (2)
38-53:asynconClick 핸들러에 try/catch가 없어 거절된 Promise가 처리되지 않을 수 있습니다.
- 문제:
handleDelete가 async이고onClick={handleDelete}로 전달되지만,openConfirm이 reject되거나 예외를 던지면 React 18 이벤트 핸들러에서 unhandled rejection이 됩니다.- 영향:
openConfirm이 정상 동작하는 현재는 낮은 위험이지만, 모달 스토어 변경 시 silent failure 가능성이 있습니다.- 대안: try/catch로 감싸거나
.catch를 체인합니다.🛡️ 수정 예시
- const handleDelete = async () => { - const confirmed = await openConfirm('개인 회고 삭제', '작성한 개인 회고를 삭제하시겠습니까?', { - confirmText: '삭제', - variant: 'danger', - }) - if (!confirmed) return - - deleteRetrospective(meetingId, { - onSuccess: () => { - navigate(ROUTES.GATHERING_DETAIL(gatheringId), { replace: true }) - }, - onError: (error) => { - openError('삭제 실패', error.userMessage) - }, - }) - } + const handleDelete = async () => { + try { + const confirmed = await openConfirm('개인 회고 삭제', '작성한 개인 회고를 삭제하시겠습니까?', { + confirmText: '삭제', + variant: 'danger', + }) + if (!confirmed) return + + deleteRetrospective(meetingId, { + onSuccess: () => { + navigate(ROUTES.GATHERING_DETAIL(gatheringId), { replace: true }) + }, + onError: (error) => { + openError('삭제 실패', error.userMessage) + }, + }) + } catch { + openError('삭제 실패', '알 수 없는 오류가 발생했습니다.') + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx` around lines 38 - 53, handleDelete is async and calls openConfirm which may reject, so wrap the await/openConfirm and subsequent deleteRetrospective call in a try/catch (or add .catch) to prevent unhandled promise rejections; specifically, in the handleDelete function, enclose the await openConfirm(...) and the deleteRetrospective(...) invocation in a try block and move the current onError logic into the catch (or call openError from catch) while keeping navigate(ROUTES.GATHERING_DETAIL(gatheringId), { replace: true }) on success; ensure any thrown error from openConfirm or deleteRetrospective is caught and reported via openError so no promise rejection is left unhandled.
77-81:data &&블록 내부에서data?.옵셔널 체이닝이 중복됩니다. 구분자 노출 엣지케이스도 있습니다.
- 문제:
{data && (...)}가드 안에서data?.meetingHeaderInfo.bookTitle처럼?.을 다시 쓰는 건 불필요합니다. 더불어bookTitle이나bookAuthor가 빈 문자열·undefined이면· authorName또는bookTitle ·형태가 노출됩니다.- 영향: 미미하지만 잘못된 타이틀/저자 조합 시 구분자가 노출됩니다.
- 대안: 옵셔널 체이닝 제거 + 양쪽 모두 값이 있을 때만
·포함.✨ 수정 예시
{data && ( <p className="typo-caption1 text-grey-600"> - {data?.meetingHeaderInfo.bookTitle} · {data?.meetingHeaderInfo.bookAuthor} + {[data.meetingHeaderInfo.bookTitle, data.meetingHeaderInfo.bookAuthor] + .filter(Boolean) + .join(' · ')} </p> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx` around lines 77 - 81, Inside the JSX block that currently checks {data && (...)} remove the redundant optional chaining (use data.meetingHeaderInfo) and change the rendering so the separator "·" is only shown when both data.meetingHeaderInfo.bookTitle and data.meetingHeaderInfo.bookAuthor are truthy; render just the title or just the author when one is missing to avoid dangling separators. Target the JSX that references data, meetingHeaderInfo, bookTitle, and bookAuthor and update the conditional output logic accordingly.
🤖 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/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx`:
- Around line 38-53: handleDelete is async and calls openConfirm which may
reject, so wrap the await/openConfirm and subsequent deleteRetrospective call in
a try/catch (or add .catch) to prevent unhandled promise rejections;
specifically, in the handleDelete function, enclose the await openConfirm(...)
and the deleteRetrospective(...) invocation in a try block and move the current
onError logic into the catch (or call openError from catch) while keeping
navigate(ROUTES.GATHERING_DETAIL(gatheringId), { replace: true }) on success;
ensure any thrown error from openConfirm or deleteRetrospective is caught and
reported via openError so no promise rejection is left unhandled.
- Around line 77-81: Inside the JSX block that currently checks {data && (...)}
remove the redundant optional chaining (use data.meetingHeaderInfo) and change
the rendering so the separator "·" is only shown when both
data.meetingHeaderInfo.bookTitle and data.meetingHeaderInfo.bookAuthor are
truthy; render just the title or just the author when one is missing to avoid
dangling separators. Target the JSX that references data, meetingHeaderInfo,
bookTitle, and bookAuthor and update the conditional output logic accordingly.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/features/retrospectives/components/PersonalRetrospectiveViewContent.tsxsrc/pages/Retrospectives/PersonalRetrospectiveViewPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/retrospectives/components/PersonalRetrospectiveViewContent.tsx
src/features/retrospectives/personal/personalRetrospective.mock.ts
Outdated
Show resolved
Hide resolved
src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx
Outdated
Show resolved
Hide resolved
src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx
Outdated
Show resolved
Hide resolved
… feat/personal-retro-90
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (5)
src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx (2)
189-189:⚠️ Potential issue | 🟡 Minor빈
className=""제거
<ExcerptBlock className="">— 빈 문자열 className은 불필요합니다.♻️ 수정 예시
- <ExcerptBlock className=""> + <ExcerptBlock>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx` at line 189, Remove the unnecessary empty className prop from the ExcerptBlock usage in PersonalRetrospectiveViewContent (i.e., replace <ExcerptBlock className=""> with <ExcerptBlock />); locate the ExcerptBlock element in the PersonalRetrospectiveViewContent.tsx render and delete the className attribute when its value is an empty string.
52-66: 스크롤 이벤트 쓰로틀링 미적용매 스크롤 이벤트마다
handleScroll이 실행됩니다. 성능 개선을 위해 쓰로틀링 적용을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx` around lines 52 - 66, The scroll handler handleScroll is attached directly so it runs on every scroll; wrap it with a throttle (or rAF debounced) wrapper before registering with window.addEventListener to reduce invocation frequency and improve performance. Create a throttled version (e.g., throttleHandleScroll) that calls handleScroll at a fixed interval or via requestAnimationFrame, use that when adding/removing the listener, and ensure you still call throttleHandleScroll once on mount (replacing the current handleScroll() call) and remove the exact throttled function in the cleanup; reference handleScroll, setActiveSection, anchors, and PERSONAL_RETRO_STICKY_OFFSET when implementing.src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts (1)
160-161: 중복 제출 가드 적용 확인
isSavePending || isUpdatePending조기 반환이 잘 적용되어 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts` around lines 160 - 161, The duplicate-submit guard in submit correctly returns early when isSavePending || isUpdatePending is true; keep this early return and ensure the flags (isSavePending and isUpdatePending) are the reactive/observable variables updated by the save and update flows (e.g., in your savePersonalRetrospective / updatePersonalRetrospective handlers) so the guard reliably prevents duplicate submissions.src/features/retrospectives/personal/hooks/useOthersPerspective.ts (1)
54-85:isItemPartial/hasPartialInput로직 중복
hasAny·hasAll판별 로직이 두 곳에서 동일하게 반복됩니다.
헬퍼 함수로 추출하면 유지보수가 쉬워집니다.♻️ 개선 예시
+function isPartialItem(item: OthersPerspectiveFormItem): boolean { + const hasAny = + item.speakerMemberId !== null || + item.topicId !== null || + item.opinion.trim() !== '' || + item.impact.trim() !== '' + const hasAll = + item.speakerMemberId !== null && + item.topicId !== null && + item.opinion.trim() !== '' && + item.impact.trim() !== '' + return hasAny && !hasAll +} const isItemPartial = useCallback( (id: string) => { const item = items.find((i) => i.id === id) if (!item) return false - const hasAny = ... - const hasAll = ... - return hasAny && !hasAll + return isPartialItem(item) }, [items] ) -const hasPartialInput = items.some((item) => { - const hasAny = ... - const hasAll = ... - return hasAny && !hasAll -}) +const hasPartialInput = items.some(isPartialItem)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/hooks/useOthersPerspective.ts` around lines 54 - 85, Extract the duplicated "hasAny/hasAll" logic into a small helper (e.g., isItemPartialFlags or computeItemCompleteness) that accepts an item and returns whether it hasAny and hasAll (performing the same speakerMemberId/topicId/null checks and opinion/impact.trim() checks), then update isItemPartial to call that helper for the found item and update hasPartialInput to call the helper for each item (or derive hasPartialInput by items.some(item => helper(item).isPartial)). Replace the inline duplicated checks in both isItemPartial and the items.some block with calls to this helper so trimming and null checks are centralized.src/features/retrospectives/personal/personalRetrospective.mock.ts (1)
50-51: base64 이미지 목데이터 개선 제안프로필 이미지에 인라인 base64가 사용되어 파일 크기가 불필요하게 늘어납니다.
https://placehold.co/100x100같은 플레이스홀더 URL로 교체하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/personalRetrospective.mock.ts` around lines 50 - 51, The profileImage field in the mock object currently contains a large inline base64 string which bloats the file; update the mock in personalRetrospective.mock.ts by replacing the profileImage base64 value with a compact placeholder URL (e.g., https://placehold.co/100x100) so the mock remains realistic but keeps the file size small — locate the profileImage property in the exported mock object and swap the long data URI for the placeholder URL.
🧹 Nitpick comments (8)
src/features/retrospectives/personal/hooks/useDeletePersonalRetrospective.ts (1)
31-34: 삭제 후 캐시 무효화 범위가 넓습니다.문제:
personalRetrospectiveQueryKeys.all전체를 무효화하고 있습니다.
영향: 현재 화면과 무관한 활성 쿼리까지 재요청될 수 있습니다.
대안: 삭제된meetingId에 해당하는 키(view,editForm) 중심으로 좁혀주세요.As per coding guidelines "queryKey 안정성, enabled 조건, select 비용, invalidate/refetch 타이밍을 중점적으로 봐줘."개선 예시
- onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: personalRetrospectiveQueryKeys.all, - }) + onSuccess: (_, meetingId) => { + queryClient.invalidateQueries({ + queryKey: personalRetrospectiveQueryKeys.view(meetingId), + }) + queryClient.invalidateQueries({ + queryKey: personalRetrospectiveQueryKeys.editForm(meetingId), + }) },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/hooks/useDeletePersonalRetrospective.ts` around lines 31 - 34, The current onSuccess invalidates personalRetrospectiveQueryKeys.all which is too broad; change it to only invalidate the specific keys for the deleted meetingId (e.g. personalRetrospectiveQueryKeys.view(meetingId) and personalRetrospectiveQueryKeys.editForm(meetingId)). In the onSuccess handler for the delete mutation in useDeletePersonalRetrospective (where queryClient.invalidateQueries is called), extract the meetingId from the mutation variables or context and call queryClient.invalidateQueries for those two specific queryKey arrays (or call invalidateQueries twice), ensuring you do not invalidate the global personalRetrospectiveQueryKeys.all.src/features/retrospectives/personal/personalRetrospective.endpoints.ts (1)
8-17: 동일 URL 템플릿이 반복됩니다.문제:
SAVE/VIEW/UPDATE/DELETE가 같은 경로 문자열을 반복합니다.
영향: 경로 변경 시 일부만 수정될 위험이 있습니다.
대안: 공통 빌더를 두고 재사용하면 유지보수가 쉬워집니다.리팩터링 예시
export const PERSONAL_RETROSPECTIVE_ENDPOINTS = { + MEETING_BASE: (meetingId: number) => + `${API_PATHS.MEETINGS}/${meetingId}/retrospectives/personal`, // 개인 회고 저장 (POST /api/meetings/{meetingId}/retrospectives/personal) - SAVE: (meetingId: number) => `${API_PATHS.MEETINGS}/${meetingId}/retrospectives/personal`, + SAVE: (meetingId: number) => PERSONAL_RETROSPECTIVE_ENDPOINTS.MEETING_BASE(meetingId), // 개인 회고 뷰 조회 (GET /api/meetings/{meetingId}/retrospectives/personal) - VIEW: (meetingId: number) => `${API_PATHS.MEETINGS}/${meetingId}/retrospectives/personal`, + VIEW: (meetingId: number) => PERSONAL_RETROSPECTIVE_ENDPOINTS.MEETING_BASE(meetingId), // 개인 회고 수정 (PUT /api/meetings/{meetingId}/retrospectives/personal) - UPDATE: (meetingId: number) => `${API_PATHS.MEETINGS}/${meetingId}/retrospectives/personal`, + UPDATE: (meetingId: number) => PERSONAL_RETROSPECTIVE_ENDPOINTS.MEETING_BASE(meetingId), // 개인 회고 삭제 (DELETE /api/meetings/{meetingId}/retrospectives/personal) - DELETE: (meetingId: number) => `${API_PATHS.MEETINGS}/${meetingId}/retrospectives/personal`, + DELETE: (meetingId: number) => PERSONAL_RETROSPECTIVE_ENDPOINTS.MEETING_BASE(meetingId), } as const🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/personalRetrospective.endpoints.ts` around lines 8 - 17, Multiple endpoint entries (SAVE, VIEW, UPDATE, DELETE) repeat the same path template; create a shared builder to avoid duplication by introducing a function or constant like personalRetrospectivePath(meetingId: number) that returns `${API_PATHS.MEETINGS}/${meetingId}/retrospectives/personal`, then replace the repeated templates in SAVE, VIEW, UPDATE, DELETE to call personalRetrospectivePath(meetingId) (keep EDIT_FORM separate since it appends /form/edit) so path changes are centralized and maintenance-safe.src/features/retrospectives/personal/components/PersonalRetrospectiveContent.tsx (1)
5-6: 동일 모듈의import type중복 분리같은 파일에서 두 번 import됩니다. 하나로 합쳐주세요.
♻️ 개선 예시
-import type { SectionKey } from '../hooks/usePersonalRetrospectiveForm' -import type { UsePersonalRetrospectiveFormReturn } from '../hooks/usePersonalRetrospectiveForm' +import type { + SectionKey, + UsePersonalRetrospectiveFormReturn, +} from '../hooks/usePersonalRetrospectiveForm'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/components/PersonalRetrospectiveContent.tsx` around lines 5 - 6, Combine the duplicate import type statements into one: replace the two separate `import type` lines that bring in SectionKey and UsePersonalRetrospectiveFormReturn from the same module with a single `import type { SectionKey, UsePersonalRetrospectiveFormReturn }` statement so both types are imported together (update the import that currently references SectionKey and the one that references UsePersonalRetrospectiveFormReturn).src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts (2)
60-83: 매 렌더마다crypto.randomUUID()불필요 호출
initialOthersPerspectiveItems·initialFreeRecordEntries는useState의 initial value로만 쓰이지만,crypto.randomUUID()를 포함한.map()연산이 매 렌더마다 실행됩니다.
→useMemo로 감싸거나, 계산을useEffect내부로 이동하세요.♻️ 개선 예시
- const initialOthersPerspectiveItems = editFormData?.retrospective.othersPerspectives.map( - (item) => ({ - id: crypto.randomUUID(), - ... - }) - ) + const initialOthersPerspectiveItems = useMemo( + () => + editFormData?.retrospective.othersPerspectives.map((item) => ({ + id: crypto.randomUUID(), + speakerMemberId: item.meetingMemberId, + topicId: item.topicId, + opinion: item.opinionContent, + impact: item.impressiveReason, + })), + [editFormData] + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts` around lines 60 - 83, initialOthersPerspectiveItems and initialFreeRecordEntries are computed each render (calling crypto.randomUUID()) even though they're only used as initial state; wrap their computation in a stable lazy initializer to avoid repeated UUID generation. Fix by moving the map logic into a useMemo (e.g., useMemo(() => ... , [editFormData])) or by providing a lazy initializer function to the corresponding useState calls inside usePersonalRetrospectiveForm so the crypto.randomUUID() calls run only once when initializing state (refer to initialOthersPerspectiveItems, initialFreeRecordEntries and the useState that consumes them).
143-158:showSection/hideSection미메모이제이션두 함수는
setVisibilityOverrides(안정적)와 서브훅 메서드(안정적useCallback)만 사용하므로,useCallback(fn, [])으로 감싸 참조 안정성을 확보할 수 있습니다.form객체가 props로 전달되는 경우 불필요한 자식 리렌더를 방지합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts` around lines 143 - 158, Wrap the showSection and hideSection functions in useCallback to stabilize their references: change the declarations of showSection and hideSection (which call setVisibilityOverrides and methods on othersPerspective, freeRecord, changedThoughts) to be memoized via useCallback(..., []) since those dependencies are stable, preserving behavior (adding/resetting items) while preventing unnecessary re-renders when the form is passed as a prop.src/features/retrospectives/personal/personalRetrospective.types.ts (1)
233-250: 수정 폼 응답의 인라인 타입을 명명 타입으로 분리해 주세요문제: Line 233-250의 하위 객체 타입이 인라인으로 중복 선언되어 있습니다.
영향: 필드 변경 시 누락/불일치가 발생하기 쉬워 유지보수 비용이 커집니다.
대안:EditChangedThoughtItem,EditOthersPerspectiveItem,EditFreeTextItem같은 명명 타입으로 추출해 재사용해 주세요.제안 코드
+export type EditChangedThoughtItem = { + topicId: number + keyIssue: string | null + preOpinion: string | null + postOpinion: string | null +} + +export type EditOthersPerspectiveItem = { + topicId: number + meetingMemberId: number + opinionContent: string + impressiveReason: string +} + +export type EditFreeTextItem = { + title: string | null + content: string | null +} + export type GetPersonalRetrospectiveEditFormResponse = { retrospectiveId: number meetingHeaderInfo: MeetingHeaderInfo topics: PersonalRetrospectiveTopic[] meetingMembers: PersonalRetrospectiveMember[] retrospective: { - changedThoughts: { - topicId: number - keyIssue: string | null - preOpinion: string | null - postOpinion: string | null - }[] - othersPerspectives: { - topicId: number - meetingMemberId: number - opinionContent: string - impressiveReason: string - }[] - freeTexts: { - title: string | null - content: string | null - }[] + changedThoughts: EditChangedThoughtItem[] + othersPerspectives: EditOthersPerspectiveItem[] + freeTexts: EditFreeTextItem[] } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/personalRetrospective.types.ts` around lines 233 - 250, The inline types under the retrospective property (changedThoughts, othersPerspectives, freeTexts) are duplicated and should be extracted into named types: create EditChangedThoughtItem for { topicId: number; keyIssue: string | null; preOpinion: string | null; postOpinion: string | null }, EditOthersPerspectiveItem for { topicId: number; meetingMemberId: number; opinionContent: string; impressiveReason: string }, and EditFreeTextItem for { title: string | null; content: string | null }, then update the retrospective type to use EditChangedThoughtItem[], EditOthersPerspectiveItem[], and EditFreeTextItem[] respectively (update any other usages of these shapes to reference the new named types).src/features/retrospectives/personal/components/ChangedThoughtsSection.tsx (2)
65-71:getPreOpinion결과를 한 번만 계산해서 재사용해 주세요문제: Line 65, 70에서 같은 인자로
getPreOpinion(item.topicId)를 중복 호출합니다.
영향: 렌더 시 불필요 호출이 늘고, 추후 구현 변경 시 분기값/표시값 불일치 가능성이 생깁니다.
대안: 루프 내부에서const preOpinion = ...으로 캐시해서 분기와value에 공통 사용해 주세요.제안 코드
{formValues.map((item) => { + const preOpinion = getPreOpinion(item.topicId) return ( <div key={item.topicId}> ... - {getPreOpinion(item.topicId) ? ( + {preOpinion ? ( <div className="grid grid-cols-2 gap-base"> ... <Textarea - value={getPreOpinion(item.topicId)} + value={preOpinion} disabled height={160} className="bg-grey-200 text-black" />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/components/ChangedThoughtsSection.tsx` around lines 65 - 71, In ChangedThoughtsSection, avoid calling getPreOpinion(item.topicId) twice; compute and reuse a local const (e.g., preOpinion) inside the render loop before the JSX branch and use that for both the conditional check and the Textarea value/disabled props so the same computed result is used in the conditional and in the component rendering.
51-56: 토픽명 조회를 반복find대신 맵 조회로 바꿔주세요문제: Line 51-56에서
formValues.map내부마다topics.find(...)를 호출합니다.
영향: 항목 수가 늘수록 렌더 비용이 불필요하게 증가합니다.
대안:topicId -> topicName맵을 1회 생성해 O(1) 조회로 바꿔주세요.제안 코드
+import { useMemo } from 'react' import { X } from 'lucide-react' ... export default function ChangedThoughtsSection({ topics, form, onClose, }: ChangedThoughtsSectionProps) { const { formValues, updateField, getPreOpinion } = form + const topicNameById = useMemo( + () => new Map(topics.map((t) => [t.topicId, t.topicName])), + [topics] + ) return ( ... - <p className="text-black typo-subtitle3 mb-small"> - {topics.find((t) => t.topicId === item.topicId)?.topicName} - </p> + <p className="text-black typo-subtitle3 mb-small"> + {topicNameById.get(item.topicId) ?? '알 수 없는 주제'} + </p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/retrospectives/personal/components/ChangedThoughtsSection.tsx` around lines 51 - 56, In ChangedThoughtsSection (inside the component rendering formValues) avoid calling topics.find(...) per item; instead build a single lookup map from topicId to topicName once (e.g., const topicNameById = new Map(...) or an object) before using formValues.map, then replace topics.find((t) => t.topicId === item.topicId)?.topicName with topicNameById.get(item.topicId) (or topicNameById[item.topicId]) and include a sensible fallback for missing names; update usages in the component (referencing formValues, topics, topicId, topicName) to use this map for O(1) lookups.
🤖 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/personal/components/ChangedThoughtsSection.tsx`:
- Around line 8-12: The props contract for ChangedThoughtsSection is
inconsistent with its documentation: update ChangedThoughtsSectionProps to
include a showErrors boolean (and any error meta required by
UseChangedThoughtsReturn) and ensure the ChangedThoughtsSection component reads
and uses showErrors to control per-field error display and initial-error
scrolling; alternatively, if error handling is intended to be external,
remove/adjust the example/comment lines 20-26 to reflect that the parent manages
errors and do not reference showErrors. Locate the interface
ChangedThoughtsSectionProps and the ChangedThoughtsSection component usage of
form (UseChangedThoughtsReturn) to add the prop and pass it through to the
form/error-rendering logic or update the docs accordingly.
In
`@src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx`:
- Around line 215-225: The current key for the freeTexts list uses
key={`${item.title ?? ''}-${item.content ?? ''}`} which can produce duplicate
keys (e.g., "-" when both title and content are null); update the freeTexts.map
usage to provide a stable fallback key (e.g., include the map index like using
map((item, index) => ...) and use index as a fallback) or preferably use a
unique identifier returned by the API (e.g., item.id) so the rendered list in
the PersonalRetrospectiveViewContent component has unique keys for each item.
- Around line 94-101: In PersonalRetrospectiveViewContent, remove the redundant
typography class by deleting 'typo-body4' from the span className so only
'typo-subtitle2' (along with the transition and color classes) remains; update
the cn call in the span that renders {label} to stop applying both 'typo-body4'
and 'typo-subtitle2' simultaneously and keep 'typo-subtitle2' for the navigation
tab styling.
In `@src/features/retrospectives/personal/hooks/usePersonalRetrospective.ts`:
- Around line 41-46: The query in usePersonalRetrospective currently sets gcTime
but omits staleTime, causing immediate staleness and unnecessary refetches;
update the useQuery call in usePersonalRetrospective to include staleTime: 10 *
60 * 1000 (matching gcTime and the behavior in usePersonalRetrospectiveView) so
the cache remains fresh for the intended period while keeping gcTime unchanged;
locate the useQuery invocation that uses personalRetrospectiveQueryKeys.detail
and getPersonalRetrospective(params) and add the staleTime property alongside
gcTime.
In
`@src/features/retrospectives/personal/hooks/usePersonalRetrospectiveEditForm.ts`:
- Around line 29-34: The isValid check in usePersonalRetrospectiveEditForm
currently uses !Number.isNaN(meetingId) && meetingId > 0 which allows Infinity;
update the validation to use Number.isFinite(meetingId) && meetingId > 0 so the
useQuery call (queryKey: personalRetrospectiveQueryKeys.editForm(meetingId),
queryFn: () => getPersonalRetrospectiveEditForm(meetingId), enabled: isValid &&
enabled) is only enabled for finite positive meetingId values.
In `@src/pages/Retrospectives/personal/PersonalRetrospectivePage.tsx`:
- Around line 101-107: The submit Button currently calls form.submit()
regardless of loading/error/data readiness; update the disabled prop to also
include the data flags (e.g., disabled={form.isSubmitting || isAnyLoading ||
isAnyError || !isDataReady}) and add a guard inside the onClick handler before
calling form.submit() (check form.isSubmitting, isAnyLoading, isAnyError, and
isDataReady) so submit is a no-op when data isn't ready or there are
loading/error conditions; reference Button, form.submit(), form.isSubmitting,
isAnyLoading, isAnyError, isDataReady to locate and modify the code.
In `@src/pages/Retrospectives/personal/PersonalRetrospectiveViewPage.tsx`:
- Around line 22-37: Compute and validate both IDs before invoking the query
hook so the hook does not run for invalid URLs: calculate isValidIds using
gatheringId and meetingId (e.g., Number.isFinite and >0) above the hooks, then
call usePersonalRetrospectiveView only when valid (or call it with a disabled
key such as meetingId = -1), or modify usePersonalRetrospectiveView to accept an
enabled option and pass enabled: isValidIds; ensure
deleteRetrospective/useDeletePersonalRetrospective usage remains after
validation so no network requests occur when gatheringId is invalid.
---
Duplicate comments:
In
`@src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx`:
- Line 189: Remove the unnecessary empty className prop from the ExcerptBlock
usage in PersonalRetrospectiveViewContent (i.e., replace <ExcerptBlock
className=""> with <ExcerptBlock />); locate the ExcerptBlock element in the
PersonalRetrospectiveViewContent.tsx render and delete the className attribute
when its value is an empty string.
- Around line 52-66: The scroll handler handleScroll is attached directly so it
runs on every scroll; wrap it with a throttle (or rAF debounced) wrapper before
registering with window.addEventListener to reduce invocation frequency and
improve performance. Create a throttled version (e.g., throttleHandleScroll)
that calls handleScroll at a fixed interval or via requestAnimationFrame, use
that when adding/removing the listener, and ensure you still call
throttleHandleScroll once on mount (replacing the current handleScroll() call)
and remove the exact throttled function in the cleanup; reference handleScroll,
setActiveSection, anchors, and PERSONAL_RETRO_STICKY_OFFSET when implementing.
In `@src/features/retrospectives/personal/hooks/useOthersPerspective.ts`:
- Around line 54-85: Extract the duplicated "hasAny/hasAll" logic into a small
helper (e.g., isItemPartialFlags or computeItemCompleteness) that accepts an
item and returns whether it hasAny and hasAll (performing the same
speakerMemberId/topicId/null checks and opinion/impact.trim() checks), then
update isItemPartial to call that helper for the found item and update
hasPartialInput to call the helper for each item (or derive hasPartialInput by
items.some(item => helper(item).isPartial)). Replace the inline duplicated
checks in both isItemPartial and the items.some block with calls to this helper
so trimming and null checks are centralized.
In `@src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts`:
- Around line 160-161: The duplicate-submit guard in submit correctly returns
early when isSavePending || isUpdatePending is true; keep this early return and
ensure the flags (isSavePending and isUpdatePending) are the reactive/observable
variables updated by the save and update flows (e.g., in your
savePersonalRetrospective / updatePersonalRetrospective handlers) so the guard
reliably prevents duplicate submissions.
In `@src/features/retrospectives/personal/personalRetrospective.mock.ts`:
- Around line 50-51: The profileImage field in the mock object currently
contains a large inline base64 string which bloats the file; update the mock in
personalRetrospective.mock.ts by replacing the profileImage base64 value with a
compact placeholder URL (e.g., https://placehold.co/100x100) so the mock remains
realistic but keeps the file size small — locate the profileImage property in
the exported mock object and swap the long data URI for the placeholder URL.
---
Nitpick comments:
In `@src/features/retrospectives/personal/components/ChangedThoughtsSection.tsx`:
- Around line 65-71: In ChangedThoughtsSection, avoid calling
getPreOpinion(item.topicId) twice; compute and reuse a local const (e.g.,
preOpinion) inside the render loop before the JSX branch and use that for both
the conditional check and the Textarea value/disabled props so the same computed
result is used in the conditional and in the component rendering.
- Around line 51-56: In ChangedThoughtsSection (inside the component rendering
formValues) avoid calling topics.find(...) per item; instead build a single
lookup map from topicId to topicName once (e.g., const topicNameById = new
Map(...) or an object) before using formValues.map, then replace topics.find((t)
=> t.topicId === item.topicId)?.topicName with topicNameById.get(item.topicId)
(or topicNameById[item.topicId]) and include a sensible fallback for missing
names; update usages in the component (referencing formValues, topics, topicId,
topicName) to use this map for O(1) lookups.
In
`@src/features/retrospectives/personal/components/PersonalRetrospectiveContent.tsx`:
- Around line 5-6: Combine the duplicate import type statements into one:
replace the two separate `import type` lines that bring in SectionKey and
UsePersonalRetrospectiveFormReturn from the same module with a single `import
type { SectionKey, UsePersonalRetrospectiveFormReturn }` statement so both types
are imported together (update the import that currently references SectionKey
and the one that references UsePersonalRetrospectiveFormReturn).
In
`@src/features/retrospectives/personal/hooks/useDeletePersonalRetrospective.ts`:
- Around line 31-34: The current onSuccess invalidates
personalRetrospectiveQueryKeys.all which is too broad; change it to only
invalidate the specific keys for the deleted meetingId (e.g.
personalRetrospectiveQueryKeys.view(meetingId) and
personalRetrospectiveQueryKeys.editForm(meetingId)). In the onSuccess handler
for the delete mutation in useDeletePersonalRetrospective (where
queryClient.invalidateQueries is called), extract the meetingId from the
mutation variables or context and call queryClient.invalidateQueries for those
two specific queryKey arrays (or call invalidateQueries twice), ensuring you do
not invalidate the global personalRetrospectiveQueryKeys.all.
In `@src/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.ts`:
- Around line 60-83: initialOthersPerspectiveItems and initialFreeRecordEntries
are computed each render (calling crypto.randomUUID()) even though they're only
used as initial state; wrap their computation in a stable lazy initializer to
avoid repeated UUID generation. Fix by moving the map logic into a useMemo
(e.g., useMemo(() => ... , [editFormData])) or by providing a lazy initializer
function to the corresponding useState calls inside usePersonalRetrospectiveForm
so the crypto.randomUUID() calls run only once when initializing state (refer to
initialOthersPerspectiveItems, initialFreeRecordEntries and the useState that
consumes them).
- Around line 143-158: Wrap the showSection and hideSection functions in
useCallback to stabilize their references: change the declarations of
showSection and hideSection (which call setVisibilityOverrides and methods on
othersPerspective, freeRecord, changedThoughts) to be memoized via
useCallback(..., []) since those dependencies are stable, preserving behavior
(adding/resetting items) while preventing unnecessary re-renders when the form
is passed as a prop.
In `@src/features/retrospectives/personal/personalRetrospective.endpoints.ts`:
- Around line 8-17: Multiple endpoint entries (SAVE, VIEW, UPDATE, DELETE)
repeat the same path template; create a shared builder to avoid duplication by
introducing a function or constant like personalRetrospectivePath(meetingId:
number) that returns
`${API_PATHS.MEETINGS}/${meetingId}/retrospectives/personal`, then replace the
repeated templates in SAVE, VIEW, UPDATE, DELETE to call
personalRetrospectivePath(meetingId) (keep EDIT_FORM separate since it appends
/form/edit) so path changes are centralized and maintenance-safe.
In `@src/features/retrospectives/personal/personalRetrospective.types.ts`:
- Around line 233-250: The inline types under the retrospective property
(changedThoughts, othersPerspectives, freeTexts) are duplicated and should be
extracted into named types: create EditChangedThoughtItem for { topicId: number;
keyIssue: string | null; preOpinion: string | null; postOpinion: string | null
}, EditOthersPerspectiveItem for { topicId: number; meetingMemberId: number;
opinionContent: string; impressiveReason: string }, and EditFreeTextItem for {
title: string | null; content: string | null }, then update the retrospective
type to use EditChangedThoughtItem[], EditOthersPerspectiveItem[], and
EditFreeTextItem[] respectively (update any other usages of these shapes to
reference the new named types).
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
src/features/retrospectives/index.tssrc/features/retrospectives/personal/components/ChangedThoughtsSection.tsxsrc/features/retrospectives/personal/components/FreeRecordSection.tsxsrc/features/retrospectives/personal/components/OthersPerspectiveSection.tsxsrc/features/retrospectives/personal/components/PersonalRetrospectiveContent.tsxsrc/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsxsrc/features/retrospectives/personal/components/index.tssrc/features/retrospectives/personal/hooks/index.tssrc/features/retrospectives/personal/hooks/personalRetrospectiveQueryKeys.tssrc/features/retrospectives/personal/hooks/useChangedThoughts.tssrc/features/retrospectives/personal/hooks/useDeletePersonalRetrospective.tssrc/features/retrospectives/personal/hooks/useFreeRecord.tssrc/features/retrospectives/personal/hooks/useOthersPerspective.tssrc/features/retrospectives/personal/hooks/usePersonalRetrospective.tssrc/features/retrospectives/personal/hooks/usePersonalRetrospectiveEditForm.tssrc/features/retrospectives/personal/hooks/usePersonalRetrospectiveForm.tssrc/features/retrospectives/personal/hooks/usePersonalRetrospectiveView.tssrc/features/retrospectives/personal/hooks/useSavePersonalRetrospective.tssrc/features/retrospectives/personal/hooks/useUpdatePersonalRetrospective.tssrc/features/retrospectives/personal/index.tssrc/features/retrospectives/personal/personalRetrospective.api.tssrc/features/retrospectives/personal/personalRetrospective.constants.tssrc/features/retrospectives/personal/personalRetrospective.endpoints.tssrc/features/retrospectives/personal/personalRetrospective.mock.tssrc/features/retrospectives/personal/personalRetrospective.types.tssrc/pages/Retrospectives/index.tssrc/pages/Retrospectives/personal/PersonalRetrospectivePage.tsxsrc/pages/Retrospectives/personal/PersonalRetrospectiveViewPage.tsxsrc/pages/Retrospectives/personal/index.ts
✅ Files skipped from review due to trivial changes (1)
- src/features/retrospectives/personal/hooks/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/pages/Retrospectives/index.ts
- src/features/retrospectives/index.ts
src/features/retrospectives/personal/components/ChangedThoughtsSection.tsx
Show resolved
Hide resolved
src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx
Show resolved
Hide resolved
src/features/retrospectives/personal/components/PersonalRetrospectiveViewContent.tsx
Show resolved
Hide resolved
src/features/retrospectives/personal/hooks/usePersonalRetrospectiveEditForm.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/shared/styles/theme.css (1)
248-256:⚠️ Potential issue | 🟠 Major
sticky-*유틸이 단독 동작하지 않고 오프셋이 하드코딩되어 있습니다.문제: Line [250], Line [255]은
top만 있고position: sticky가 없습니다. 또한 Line [255]의65px,24px는 토큰 없이 하드코딩입니다.
영향: 유틸 단독 사용 시 sticky가 동작하지 않아 UI 고정 동작이 깨질 수 있고, 디자인 변경 시 유지보수 비용이 커집니다.
대안: 두 유틸에position: sticky를 포함하고, 오프셋 값을 토큰으로 분리해calc()에 사용하세요.♻️ 제안 수정
@@ --spacing-gnb-height: 64px; --spacing-sub-page-header-height: 59px; + --spacing-retro-page-header-height: 65px; + --spacing-retro-view-nav-padding-top: 24px; @@ `@utility` sticky-below-subheader { /* GNB + SubPageHeader */ + position: sticky; top: calc(var(--spacing-gnb-height) + var(--spacing-sub-page-header-height)); } @@ `@utility` sticky-retro-view-nav { - /* GNB + SubPageHeader + 페이지 헤더 바(65px) + 패딩(24px) */ - top: calc(var(--spacing-gnb-height) + var(--spacing-sub-page-header-height) + 65px + 24px); + /* GNB + SubPageHeader + 페이지 헤더 바 + 패딩 */ + position: sticky; + top: calc( + var(--spacing-gnb-height) + + var(--spacing-sub-page-header-height) + + var(--spacing-retro-page-header-height) + + var(--spacing-retro-view-nav-padding-top) + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/styles/theme.css` around lines 248 - 256, The two utilities `@utility` sticky-below-subheader and `@utility` sticky-retro-view-nav are missing position: sticky and the retro nav offsets use hardcoded values; update both utilities to include position: sticky and replace the hardcoded 65px and 24px in sticky-retro-view-nav with CSS token variables (e.g. --spacing-page-header-height and --spacing-page-header-padding or existing equivalent tokens), then use calc(...) combining var(--spacing-gnb-height), var(--spacing-sub-page-header-height) and the new tokens so the utilities work standalone and offsets are theme-driven.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/shared/styles/theme.css`:
- Around line 248-256: The two utilities `@utility` sticky-below-subheader and
`@utility` sticky-retro-view-nav are missing position: sticky and the retro nav
offsets use hardcoded values; update both utilities to include position: sticky
and replace the hardcoded 65px and 24px in sticky-retro-view-nav with CSS token
variables (e.g. --spacing-page-header-height and --spacing-page-header-padding
or existing equivalent tokens), then use calc(...) combining
var(--spacing-gnb-height), var(--spacing-sub-page-header-height) and the new
tokens so the utilities work standalone and offsets are theme-driven.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/features/retrospectives/components/RetrospectiveCardButtons.tsxsrc/features/retrospectives/index.tssrc/pages/Retrospectives/index.tssrc/routes/index.tsxsrc/shared/constants/routes.tssrc/shared/styles/theme.css
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/retrospectives/index.ts
* [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>
🚀 풀 리퀘스트 제안
📋 작업 내용
개인 회고 기능의 전체 CRUD를 구현했습니다. 작성 화면 UI와 폼 상태 관리, 저장/수정/삭제 API 연동, 그리고 작성된 회고를 읽기 전용으로 보여주는 조회 화면까지 포함합니다.
🔧 변경 사항
개인 회고 작성 화면
개인 회고 저장 (POST)
개인 회고 조회 화면
개인 회고 수정 (PUT)
개인 회고 삭제 (DELETE)
API / 타입 / 엔드포인트
스타일
📸 스크린샷 (선택 사항)
📄 기타
Summary by CodeRabbit
릴리스 노트
새 기능
버그 수정