Repository navigation
feat: 답글 관련 에러 해결 - #265
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
|
@coderabbitai 여기에서 파일이 너무 긴데, 어떻게 리팩토링을 진행하면 될까요? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainI'll fetch the PR details to identify which file you're referring to. 🏁 Script executed: Length of output: 799 🏁 Script executed: Length of output: 8931
📁 추천 파일 구조또는 ✂️ 분리 상세 안1.
|
| 분리 대상 | 이유 |
|---|---|
useClickOutside |
여러 컴포넌트에서 재사용 가능한 범용 훅 |
CommentHeader |
프로필/날짜 UI가 독립적으로 테스트·수정 가능 |
CommentMenu |
가장 복잡한 JSX 블록, 드롭다운 상태도 함께 내부화 |
CommentEditForm |
수정 UI가 독립적인 책임 단위 |
이렇게 분리하면 comment_item.tsx가 ~230줄 → ~70줄 수준으로 줄어들고, 각 컴포넌트를 독립적으로 유지보수할 수 있습니다.
There was a problem hiding this comment.
Code Review
This pull request introduces an isMine prop to the CommentItem component to handle conditional rendering of edit, delete, and report actions based on comment ownership. It also refactors the dropdown menu logic to use derived permissions. A potential issue was identified where the report action might be shown even if disabled by the parent component; a suggestion was provided to ensure the canReport prop is respected.
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (6)
src/utils/date.ts (1)
1-7: Optional: guard against invalid ISO input.
formatDate("not a date")will renderNaN.NaN.NaNsince theDatemethods returnNaNandpadStartkeeps the 3-char string. If upstream can pass bad values, consider returning an empty string (or the original) whenisNaN(date.getTime()). Also, an explicit: stringreturn type would make the helper slightly more discoverable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/date.ts` around lines 1 - 7, The formatDate function should guard against invalid ISO input and declare an explicit return type: in formatDate(iso: string): string, after constructing const date = new Date(iso) check if isNaN(date.getTime()) and if so return a safe fallback (e.g., "" or the original iso) instead of proceeding to compute year/month/day; otherwise continue to format using year/month/day as before. This ensures formatDate does not return "NaN.NaN.NaN" for bad inputs and makes the return type explicit.src/hooks/useClickOutside.ts (1)
3-18: Consider stabilizinghandlerand handling touch events.Two small improvements:
Including
handlerin the dep array means the listener is detached/reattached on every render where the consumer passes a non-memoized inline function. A common pattern is to keephandlerin a ref so the effect can depend only onref:♻️ Suggested refactor
-import { useEffect, RefObject } from "react"; +import { useEffect, useRef, RefObject } from "react"; export function useClickOutside( ref: RefObject<HTMLElement | null>, handler: () => void ) { + const handlerRef = useRef(handler); + useEffect(() => { + handlerRef.current = handler; + }, [handler]); + useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { + const handleClickOutside = (e: MouseEvent | TouchEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) { - handler(); + handlerRef.current(); } }; document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("touchstart", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("touchstart", handleClickOutside); }; - }, [ref, handler]); + }, [ref]); }Adding
touchstartimproves mobile behavior for dropdowns that close on outside tap (consistent with common libraries like Headless UI / Radix).Both are optional if consumers always memoize
handlerand only need desktop behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useClickOutside.ts` around lines 3 - 18, The effect in useClickOutside re-attaches on every render when consumers pass a non-memoized handler and also misses touch events; fix it by storing the incoming handler in a ref (e.g., const handlerRef = useRef(handler); update handlerRef.current in an effect) and use handlerRef.current inside the internal handleClickOutside, change document.addEventListener/removeEventListener to register both "mousedown" and "touchstart", and update the effect dependency array to only depend on ref (not handler) while ensuring cleanup removes both listeners for handleClickOutside.src/components/base-ui/Comment/comment_edit_form.tsx (1)
14-39: Optional: disable Save on empty input and support keyboard shortcuts.Minor UX improvements — none are blockers:
- Disable
저장whenvalue.trim()is empty to avoid sending empty updates (assuming the parent doesn't already prevent that).- Consider
Esc→onCancel,Ctrl/Cmd+Enter→onSavefor faster editing.💡 Sketch
- <textarea - value={value} - onChange={(e) => onChange(e.target.value)} + <textarea + value={value} + onChange={(e) => onChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape") onCancel(); + if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && value.trim()) onSave(); + }} className="..." autoFocus /> @@ - <button - type="button" - onClick={onSave} - className="px-4 py-2 rounded-lg bg-primary-3 text-White subhead_4_1" - > + <button + type="button" + onClick={onSave} + disabled={!value.trim()} + className="px-4 py-2 rounded-lg bg-primary-3 text-White subhead_4_1 disabled:opacity-50 disabled:cursor-not-allowed" + > 저장 </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Comment/comment_edit_form.tsx` around lines 14 - 39, The Save button should be disabled when the edited text is empty and keyboard shortcuts should be supported: update the component (the JSX around the textarea and the save button in comment_edit_form.tsx, e.g., the component using props value, onChange, onSave, onCancel) to set the Save button disabled when value.trim() === "" and add a onKeyDown handler on the textarea that calls onCancel when Escape is pressed and calls onSave when Ctrl+Enter or Meta+Enter is pressed; ensure the onClick for the Save button still calls onSave and the disabled state prevents empty submissions (and add appropriate aria-disabled/disabled attributes).src/components/base-ui/Comment/comment_menu.tsx (3)
52-125: Repetitive button markup can be extracted.The four action buttons share identical structure (
type, className,onClickthat calls handler +onClose,<Image>icon, label). A smallMenuItemsub-component or array-driven render would remove duplication and make it easier to add/remove actions consistently.♻️ Sketch
const MenuItem = ({ icon, label, onSelect }: { icon: string; label: string; onSelect: () => void; }) => ( <button type="button" onClick={() => { onSelect(); onClose(); }} role="menuitem" className="flex w-full h-[44px] items-center justify-center gap-2 Body_1_2 text-Gray-4 hover:bg-Gray-1 cursor-pointer" > <Image src={icon} alt="" width={24} height={24} /> {label} </button> );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Comment/comment_menu.tsx` around lines 52 - 125, The Comment menu renders four nearly identical action buttons (reply, delete, edit, report) in comment_menu.tsx leading to duplicated markup; refactor by extracting a MenuItem subcomponent (or map an actions array) that accepts icon, label and onSelect and use it for onReply/onDelete/onEdit/onReport while retaining the onClose call (i.e., MenuItem calls onSelect() then onClose()), keep className, type and Image usage identical, and replace the repeated JSX blocks with MenuItem instances to simplify logic and maintain conditional rendering around hasReplyAction, hasManageAction, canDelete, canEdit, and canReport.
48-50: Consider addingrole="menu"to the dropdown androle="menuitem"to each action button.This improves keyboard/AT semantics now that the dropdown is its own component. Also, line 50 contains a stray whitespace-only line inside the dropdown (harmless but noisy).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Comment/comment_menu.tsx` around lines 48 - 50, Add accessible roles to the dropdown and its actions: on the dropdown container element rendered when open (in CommentMenu / comment_menu.tsx where the div with className "... w-[137px] rounded-lg bg-White shadow-lg z-10 overflow-hidden" is created) add role="menu", and add role="menuitem" to each action button element rendered inside that dropdown (the action elements in the same file). Also remove the stray blank/whitespace-only line inside the dropdown markup to clean up the JSX.
72-125:canReportis silently suppressed wheneverhasManageActionis true.The
hasManageAction ? (...) : (canReport && ...)branch makes "신고하기" unreachable if eithercanEditorcanDeleteis true — even whencanReportis also true. In the currentCommentItemthis is masked becausederivedCanReportis!isMine(opposite ofisMine), but the component contract is stricter than its implementation suggests. Either:
- Document this mutual exclusivity in
CommentMenuProps(e.g., note thatcanReportis ignored whencanEdit || canDelete), or- Render report independently (with its own separator), so this component behaves predictably regardless of caller assumptions:
♻️ Suggested structure
- {/* 내 댓글일 경우 삭제/수정, 아닐 경우 신고 */} - {hasManageAction ? ( - <> - {canDelete && ( ... )} - {canEdit && canDelete && ( <div className="mx-2 border-b border-Subbrown-4" /> )} - {canEdit && ( ... )} - </> - ) : ( - canReport && ( ... ) - )} + {canDelete && ( /* 삭제하기 button */ )} + {canEdit && canDelete && (<div className="mx-2 border-b border-Subbrown-4" />)} + {canEdit && ( /* 수정하기 button */ )} + {canReport && hasManageAction && (<div className="mx-2 border-b border-Subbrown-4" />)} + {canReport && ( /* 신고하기 button */ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Comment/comment_menu.tsx` around lines 72 - 125, The current conditional in CommentMenu (using hasManageAction ? (...) : (canReport && ...)) hides the "신고하기" button whenever hasManageAction is true even if canReport is true; update the component so report rendering is independent: inside the CommentMenu component, always check canReport separately (regardless of hasManageAction) and render the report button (with a separating divider when it coexists with manage actions), or alternatively document the exclusivity in CommentMenuProps by adding a clear comment/TS doc that canReport is ignored when canEdit || canDelete; refer to hasManageAction, canReport, canEdit, canDelete and CommentMenuProps when making the change.
🤖 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/components/base-ui/Comment/comment_header.tsx`:
- Around line 20-42: The clickable profile wrapper currently uses a div with
onClick and is not keyboard-accessible; when onProfileClick is provided, change
the wrapper that contains the avatar, authorName and isAuthor badge to a
semantic interactive element (e.g., render a button or add role="button"
tabIndex={0} and handle Enter/Space in onKeyDown) so keyboard and screen-reader
users can activate it; specifically update the element wrapping Image,
authorName and the isAuthor span (the block that checks onProfileClick and has
onClick={() => onProfileClick?.(authorName)}) to include proper focus,
aria-label if needed, and keyboard handlers while preserving the existing
className and onClick behavior.
- Around line 4-10: The profile region in CommentHeader (the clickable div
wrapping the profile image and name) is not keyboard accessible; update the
element so keyboard users can activate it: either replace the div with a
semantic <button> or add role="button", tabIndex={0}, and an onKeyDown handler
that listens for Enter and Space and calls the existing
onProfileClick(authorName) callback; also ensure the click handler still calls
onProfileClick and add an appropriate aria-label or use the authorName for
screen-reader context.
In `@src/components/base-ui/Comment/comment_item.tsx`:
- Around line 50-52: The current useEffect in Comment (useEffect(() => {
setEditContent(content); }, [content])) unconditionally overwrites editContent
whenever the content prop changes, which can wipe a user's in-progress draft;
update the effect in the Comment component to only sync prop content into state
when not currently editing (check isEditing) so setEditContent(content) runs
only if !isEditing, ensuring editContent is preserved while the user is editing
and still updated on external changes when not editing.
- Around line 61-63: The derivedCanReport logic wrongly enables reporting for
all non-owners regardless of the canReport prop; change the expression used to
compute derivedCanReport to require both that the commenter is not the owner and
that canReport is true (e.g., derivedCanReport = canReport && !isMine) and
update the Comment component's prop default for canReport (or make canReport
required) so callers who set canReport={false} actually disable the report
action; ensure any usage of derivedCanReport and onReport (e.g., the report
button render/handler) relies on this corrected value.
In `@src/components/base-ui/Comment/comment_menu.tsx`:
- Around line 39-45: The menu trigger button lacks an accessible name and menu
semantics; update the <button> in comment_menu.tsx (the element using onToggle
and the Image) to include an explicit accessible name (e.g. aria-label="메뉴" or
aria-labelledby), add aria-haspopup="menu" and aria-expanded tied to the
component's open state (e.g. aria-expanded={isOpen} where isOpen is a prop or
state), and make the Image decorative (alt="") so it doesn't duplicate the
label; ensure onToggle still toggles the same isOpen state.
In `@src/components/base-ui/Comment/comment_section_bookcase.tsx`:
- Line 238: The onProfileClick handler currently calls
router.push(`/profile/${nickname}`) with the raw nickname; update the handler
used in CommentSection/CommentHeader so it calls router.push with the
URL-encoded nickname (e.g., use encodeURIComponent on the nickname before
interpolating) and add a small guard for the `(알 수 없음)` fallback (skip
navigation or map it to a safe canonical value) to avoid malformed routes;
verify the value passed in from CommentHeader's authorName (and mapping in
mapApiToUiComments) is the backend nickname expected by /profile/[nickname] and
use that encoded value when invoking router.push.
In `@src/components/base-ui/Comment/comment_section_notice.tsx`:
- Line 231: The onProfileClick handler in CommentSectionNotice uses
router.push(`/profile/${nickname}`) which can break for nicknames with
URL-reserved characters; update the handler to call
router.push(`/profile/${encodeURIComponent(nickname)}`) (or encodeURIComponent
before interpolation) so the nickname is URL-encoded when navigating from the
CommentSectionNotice component's onProfileClick prop.
---
Nitpick comments:
In `@src/components/base-ui/Comment/comment_edit_form.tsx`:
- Around line 14-39: The Save button should be disabled when the edited text is
empty and keyboard shortcuts should be supported: update the component (the JSX
around the textarea and the save button in comment_edit_form.tsx, e.g., the
component using props value, onChange, onSave, onCancel) to set the Save button
disabled when value.trim() === "" and add a onKeyDown handler on the textarea
that calls onCancel when Escape is pressed and calls onSave when Ctrl+Enter or
Meta+Enter is pressed; ensure the onClick for the Save button still calls onSave
and the disabled state prevents empty submissions (and add appropriate
aria-disabled/disabled attributes).
In `@src/components/base-ui/Comment/comment_menu.tsx`:
- Around line 52-125: The Comment menu renders four nearly identical action
buttons (reply, delete, edit, report) in comment_menu.tsx leading to duplicated
markup; refactor by extracting a MenuItem subcomponent (or map an actions array)
that accepts icon, label and onSelect and use it for
onReply/onDelete/onEdit/onReport while retaining the onClose call (i.e.,
MenuItem calls onSelect() then onClose()), keep className, type and Image usage
identical, and replace the repeated JSX blocks with MenuItem instances to
simplify logic and maintain conditional rendering around hasReplyAction,
hasManageAction, canDelete, canEdit, and canReport.
- Around line 48-50: Add accessible roles to the dropdown and its actions: on
the dropdown container element rendered when open (in CommentMenu /
comment_menu.tsx where the div with className "... w-[137px] rounded-lg bg-White
shadow-lg z-10 overflow-hidden" is created) add role="menu", and add
role="menuitem" to each action button element rendered inside that dropdown (the
action elements in the same file). Also remove the stray blank/whitespace-only
line inside the dropdown markup to clean up the JSX.
- Around line 72-125: The current conditional in CommentMenu (using
hasManageAction ? (...) : (canReport && ...)) hides the "신고하기" button whenever
hasManageAction is true even if canReport is true; update the component so
report rendering is independent: inside the CommentMenu component, always check
canReport separately (regardless of hasManageAction) and render the report
button (with a separating divider when it coexists with manage actions), or
alternatively document the exclusivity in CommentMenuProps by adding a clear
comment/TS doc that canReport is ignored when canEdit || canDelete; refer to
hasManageAction, canReport, canEdit, canDelete and CommentMenuProps when making
the change.
In `@src/hooks/useClickOutside.ts`:
- Around line 3-18: The effect in useClickOutside re-attaches on every render
when consumers pass a non-memoized handler and also misses touch events; fix it
by storing the incoming handler in a ref (e.g., const handlerRef =
useRef(handler); update handlerRef.current in an effect) and use
handlerRef.current inside the internal handleClickOutside, change
document.addEventListener/removeEventListener to register both "mousedown" and
"touchstart", and update the effect dependency array to only depend on ref (not
handler) while ensuring cleanup removes both listeners for handleClickOutside.
In `@src/utils/date.ts`:
- Around line 1-7: The formatDate function should guard against invalid ISO
input and declare an explicit return type: in formatDate(iso: string): string,
after constructing const date = new Date(iso) check if isNaN(date.getTime()) and
if so return a safe fallback (e.g., "" or the original iso) instead of
proceeding to compute year/month/day; otherwise continue to format using
year/month/day as before. This ensures formatDate does not return "NaN.NaN.NaN"
for bad inputs and makes the return type explicit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e576d4a6-89c4-4c5c-b141-46ffa14f0077
📒 Files selected for processing (11)
src/components/base-ui/Comment/comment_edit_form.tsxsrc/components/base-ui/Comment/comment_header.tsxsrc/components/base-ui/Comment/comment_item.tsxsrc/components/base-ui/Comment/comment_list.tsxsrc/components/base-ui/Comment/comment_list_notice.tsxsrc/components/base-ui/Comment/comment_menu.tsxsrc/components/base-ui/Comment/comment_section.tsxsrc/components/base-ui/Comment/comment_section_bookcase.tsxsrc/components/base-ui/Comment/comment_section_notice.tsxsrc/hooks/useClickOutside.tssrc/utils/date.ts
💤 Files with no reviewable changes (1)
- src/components/base-ui/Comment/comment_section.tsx
| type CommentHeaderProps = { | ||
| authorName: string; | ||
| profileImgSrc: string; | ||
| isAuthor: boolean; | ||
| createdAt: string; | ||
| onProfileClick?: (nickname: string) => void; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm whether any caller of CommentHeader / CommentItem can pass undefined profileImgSrc.
rg -nP --type=ts --type=tsx -C2 'profileImgSrc'
rg -nP -C2 'profileImageUrl' --type=ts --type=tsxRepository: checkmo2025/FE
Length of output: 114
🏁 Script executed:
#!/bin/bash
# Search for profileImgSrc and profileImageUrl without invalid type flags
echo "=== Searching for profileImgSrc usage ==="
rg -n 'profileImgSrc' --type-list | head -20
echo ""
echo "=== Searching for profileImgSrc in tsx/ts files ==="
rg -n 'profileImgSrc' -g '*.tsx' -g '*.ts' -C 2
echo ""
echo "=== Searching for profileImageUrl ==="
rg -n 'profileImageUrl' -g '*.tsx' -g '*.ts' -C 2Repository: checkmo2025/FE
Length of output: 50778
🏁 Script executed:
#!/bin/bash
# List the relevant files first
echo "=== Looking for comment_header.tsx ==="
fd -t f 'comment_header'
echo ""
echo "=== Looking for comment files ==="
fd -t f 'comment_' src/components/base-ui/Comment/Repository: checkmo2025/FE
Length of output: 651
🏁 Script executed:
#!/bin/bash
# Read the comment_header.tsx file to see the type definition and Image usage
cat -n src/components/base-ui/Comment/comment_header.tsx | head -50Repository: checkmo2025/FE
Length of output: 1867
🏁 Script executed:
#!/bin/bash
# Check where CommentHeader is imported/used
rg -n 'CommentHeader' -g '*.tsx' -g '*.ts' | grep -v 'node_modules'Repository: checkmo2025/FE
Length of output: 477
🏁 Script executed:
#!/bin/bash
# View the full comment_item.tsx to understand the default value handling
cat -n src/components/base-ui/Comment/comment_item.tsx | head -80Repository: checkmo2025/FE
Length of output: 2721
Add keyboard accessibility to the clickable profile region.
The clickable div that wraps the profile image and name (lines 21–42) lacks keyboard support. It should be focusable and respond to Enter/Space keys for users relying on keyboard navigation or screen readers. Either convert it to a <button> element or add role="button", tabIndex={0}, and an onKeyDown handler to make it fully accessible.
🛠️ Suggested fix
- <div
- className={`flex items-center gap-2 ${onProfileClick ? 'cursor-pointer group hover:bg-Gray-1 transition-colors rounded-lg px-2 py-1.5 -ml-2' : ''}`}
- onClick={() => onProfileClick?.(authorName)}
+ <div
+ role={onProfileClick ? "button" : undefined}
+ tabIndex={onProfileClick ? 0 : undefined}
+ className={`flex items-center gap-2 ${onProfileClick ? 'cursor-pointer group hover:bg-Gray-1 transition-colors rounded-lg px-2 py-1.5 -ml-2' : ''}`}
+ onClick={() => onProfileClick?.(authorName)}
+ onKeyDown={(e) => {
+ if (onProfileClick && (e.key === 'Enter' || e.key === ' ')) {
+ e.preventDefault();
+ onProfileClick(authorName);
+ }
+ }}
>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_header.tsx` around lines 4 - 10, The
profile region in CommentHeader (the clickable div wrapping the profile image
and name) is not keyboard accessible; update the element so keyboard users can
activate it: either replace the div with a semantic <button> or add
role="button", tabIndex={0}, and an onKeyDown handler that listens for Enter and
Space and calls the existing onProfileClick(authorName) callback; also ensure
the click handler still calls onProfileClick and add an appropriate aria-label
or use the authorName for screen-reader context.
| <div className="flex items-center justify-between"> | ||
| <div | ||
| className={`flex items-center gap-2 ${onProfileClick ? 'cursor-pointer group hover:bg-Gray-1 transition-colors rounded-lg px-2 py-1.5 -ml-2' : ''}`} | ||
| onClick={() => onProfileClick?.(authorName)} | ||
| > | ||
| <div className={`relative w-8 h-8 shrink-0 rounded-full overflow-hidden transition-opacity ${onProfileClick ? 'group-hover:opacity-80' : ''}`}> | ||
| <Image | ||
| src={profileImgSrc} | ||
| alt={authorName} | ||
| fill | ||
| className="object-cover" | ||
| sizes="32px" | ||
| /> | ||
| </div> | ||
| <span className={`Subhead_4_1 text-Gray-7 transition-opacity ${onProfileClick ? 'group-hover:opacity-80' : ''}`}> | ||
| {authorName} | ||
| </span> | ||
| {isAuthor && ( | ||
| <span className={`px-2 py-0.5 Subhead_4 text-Gray-3 transition-opacity ${onProfileClick ? 'group-hover:opacity-80' : ''}`}> | ||
| 작성자 | ||
| </span> | ||
| )} | ||
| </div> |
There was a problem hiding this comment.
Make the clickable profile region keyboard-accessible.
The profile area uses a <div> with onClick but no focus/keyboard semantics, so keyboard and screen-reader users cannot trigger navigation. When onProfileClick is provided, render it as a <button> (or add role="button", tabIndex={0}, and onKeyDown for Enter/Space).
♿ Suggested fix
- <div
- className={`flex items-center gap-2 ${onProfileClick ? 'cursor-pointer group hover:bg-Gray-1 transition-colors rounded-lg px-2 py-1.5 -ml-2' : ''}`}
- onClick={() => onProfileClick?.(authorName)}
- >
+ {onProfileClick ? (
+ <button
+ type="button"
+ onClick={() => onProfileClick(authorName)}
+ className="flex items-center gap-2 cursor-pointer group hover:bg-Gray-1 transition-colors rounded-lg px-2 py-1.5 -ml-2 text-left"
+ aria-label={`${authorName} 프로필 보기`}
+ >
+ {/* ...content... */}
+ </button>
+ ) : (
+ <div className="flex items-center gap-2">
+ {/* ...content... */}
+ </div>
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_header.tsx` around lines 20 - 42, The
clickable profile wrapper currently uses a div with onClick and is not
keyboard-accessible; when onProfileClick is provided, change the wrapper that
contains the avatar, authorName and isAuthor badge to a semantic interactive
element (e.g., render a button or add role="button" tabIndex={0} and handle
Enter/Space in onKeyDown) so keyboard and screen-reader users can activate it;
specifically update the element wrapping Image, authorName and the isAuthor span
(the block that checks onProfileClick and has onClick={() =>
onProfileClick?.(authorName)}) to include proper focus, aria-label if needed,
and keyboard handlers while preserving the existing className and onClick
behavior.
| useEffect(() => { | ||
| setEditContent(content); | ||
| }, [content]); |
There was a problem hiding this comment.
editContent can be wiped mid-edit when content prop updates.
If the parent re-renders with a new content (e.g., after a successful edit elsewhere, optimistic update, or realtime refresh) while isEditing is true, this effect overwrites the in-progress draft. Consider gating the sync on !isEditing:
♻️ Proposed tweak
useEffect(() => {
- setEditContent(content);
-}, [content]);
+ if (!isEditing) setEditContent(content);
+}, [content, isEditing]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| setEditContent(content); | |
| }, [content]); | |
| useEffect(() => { | |
| if (!isEditing) setEditContent(content); | |
| }, [content, isEditing]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_item.tsx` around lines 50 - 52, The
current useEffect in Comment (useEffect(() => { setEditContent(content); },
[content])) unconditionally overwrites editContent whenever the content prop
changes, which can wipe a user's in-progress draft; update the effect in the
Comment component to only sync prop content into state when not currently
editing (check isEditing) so setEditContent(content) runs only if !isEditing,
ensuring editContent is preserved while the user is editing and still updated on
external changes when not editing.
| const derivedCanEdit = canEdit || isMine; | ||
| const derivedCanDelete = canDelete || isMine; | ||
| const derivedCanReport = canReport || !isMine; |
There was a problem hiding this comment.
derivedCanReport ignores the canReport prop for non-owners.
canReport || !isMine means whenever isMine is false, the "신고하기" action is forced on regardless of what the caller passed for canReport. The canReport prop is effectively only honored for the user's own comments (where it is unlikely to matter). If the intent is "owners can't report themselves, otherwise defer to the caller", consider:
♻️ Proposed tweak
-const derivedCanReport = canReport || !isMine;
+const derivedCanReport = !isMine && (canReport ?? true);…and change the canReport prop default accordingly (or make it required). Otherwise, a caller that explicitly sets canReport={false} for logged‑out / guest users still gets a functional report button that dispatches to onReport?.(id) (a no‑op) — confusing UX.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_item.tsx` around lines 61 - 63, The
derivedCanReport logic wrongly enables reporting for all non-owners regardless
of the canReport prop; change the expression used to compute derivedCanReport to
require both that the commenter is not the owner and that canReport is true
(e.g., derivedCanReport = canReport && !isMine) and update the Comment
component's prop default for canReport (or make canReport required) so callers
who set canReport={false} actually disable the report action; ensure any usage
of derivedCanReport and onReport (e.g., the report button render/handler) relies
on this corrected value.
| <button | ||
| type="button" | ||
| onClick={onToggle} | ||
| className="p-1 cursor-pointer" | ||
| > | ||
| <Image src="/menu_dots.svg" alt="메뉴" width={20} height={20} /> | ||
| </button> |
There was a problem hiding this comment.
Add a11y attributes to the menu trigger.
The trigger button has no accessible name (the <Image> alt="메뉴" doesn't name the button) and no aria-expanded / aria-haspopup. Screen readers will announce it as an unlabeled button with no indication of its menu semantics.
♻️ Proposed tweak
<button
type="button"
onClick={onToggle}
+ aria-label="댓글 메뉴"
+ aria-haspopup="menu"
+ aria-expanded={open}
className="p-1 cursor-pointer"
>
<Image src="/menu_dots.svg" alt="메뉴" width={20} height={20} />
</button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_menu.tsx` around lines 39 - 45, The
menu trigger button lacks an accessible name and menu semantics; update the
<button> in comment_menu.tsx (the element using onToggle and the Image) to
include an explicit accessible name (e.g. aria-label="메뉴" or aria-labelledby),
add aria-haspopup="menu" and aria-expanded tied to the component's open state
(e.g. aria-expanded={isOpen} where isOpen is a prop or state), and make the
Image decorative (alt="") so it doesn't duplicate the label; ensure onToggle
still toggles the same isOpen state.
| onEditComment={handleEditComment} | ||
| onDeleteComment={handleDeleteComment} | ||
| onReportComment={handleReportComment} | ||
| onProfileClick={(nickname) => router.push(`/profile/${nickname}`)} |
There was a problem hiding this comment.
Encode the nickname before routing.
router.push(\/profile/${nickname}`)passes the nickname verbatim. If a nickname ever contains characters like/, ?, #, %`, or whitespace, the resulting URL will be malformed or resolve to the wrong dynamic segment. Safer:
🛠️ Suggested fix
- onProfileClick={(nickname) => router.push(`/profile/${nickname}`)}
+ onProfileClick={(nickname) =>
+ router.push(`/profile/${encodeURIComponent(nickname)}`)
+ }Also note: onProfileClick here is invoked with authorName from CommentHeader (not necessarily the canonical member nickname). Double-check that authorName is always the backend nickname used by the /profile/[nickname] route (it does seem to be, per mapApiToUiComments, but worth confirming for the (알 수 없음) fallback case, which would currently route to /profile/(알 수 없음)).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onProfileClick={(nickname) => router.push(`/profile/${nickname}`)} | |
| onProfileClick={(nickname) => | |
| router.push(`/profile/${encodeURIComponent(nickname)}`) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_section_bookcase.tsx` at line 238, The
onProfileClick handler currently calls router.push(`/profile/${nickname}`) with
the raw nickname; update the handler used in CommentSection/CommentHeader so it
calls router.push with the URL-encoded nickname (e.g., use encodeURIComponent on
the nickname before interpolating) and add a small guard for the `(알 수 없음)`
fallback (skip navigation or map it to a safe canonical value) to avoid
malformed routes; verify the value passed in from CommentHeader's authorName
(and mapping in mapApiToUiComments) is the backend nickname expected by
/profile/[nickname] and use that encoded value when invoking router.push.
| onEditComment={handleEditComment} | ||
| onDeleteComment={handleDeleteComment} | ||
| onReportComment={handleReportComment} | ||
| onProfileClick={(nickname) => router.push(`/profile/${nickname}`)} |
There was a problem hiding this comment.
Same encoding concern as comment_section_bookcase.tsx.
Consider encodeURIComponent(nickname) here as well so nicknames containing URL-reserved characters don't break /profile/[nickname] routing.
🛠️ Suggested fix
- onProfileClick={(nickname) => router.push(`/profile/${nickname}`)}
+ onProfileClick={(nickname) =>
+ router.push(`/profile/${encodeURIComponent(nickname)}`)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onProfileClick={(nickname) => router.push(`/profile/${nickname}`)} | |
| onProfileClick={(nickname) => | |
| router.push(`/profile/${encodeURIComponent(nickname)}`) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_section_notice.tsx` at line 231, The
onProfileClick handler in CommentSectionNotice uses
router.push(`/profile/${nickname}`) which can break for nicknames with
URL-reserved characters; update the handler to call
router.push(`/profile/${encodeURIComponent(nickname)}`) (or encodeURIComponent
before interpolation) so the nickname is URL-encoded when navigating from the
CommentSectionNotice component's onProfileClick prop.
📌 개요 (Summary)
🛠️ 변경 사항 (Changes)
📸 스크린샷 (Screenshots)
(UI 변경 사항이 있다면 첨부해주세요)
✅ 체크리스트 (Checklist)
pnpm build)pnpm lint)Summary by CodeRabbit