Skip to content

feat: 답글 관련 에러 해결 - #265

Merged
shinwokkang merged 2 commits into
devfrom
fix/264-my-comment-ham
Apr 24, 2026
Merged

shinwokkang merged 2 commits into
devfrom
fix/264-my-comment-ham

Conversation

@shinwokkang

@shinwokkang shinwokkang commented Apr 22, 2026 •

Copy link
Copy Markdown
Contributor

📌 개요 (Summary)

  • 변경 사항에 대한 간략한 요약을 적어주세요.
  • 관련 이슈가 있다면 링크를 걸어주세요 (예: [fix] cicd / build 문제 #123).

🛠️ 변경 사항 (Changes)

  • 새로운 기능 추가
  • 버그 수정
  • 코드 리팩토링
  • 문서 업데이트
  • 기타 (설명: )

📸 스크린샷 (Screenshots)

(UI 변경 사항이 있다면 첨부해주세요)

✅ 체크리스트 (Checklist)

  • 빌드가 성공적으로 수행되었나요? (pnpm build)
  • 린트 에러가 없나요? (pnpm lint)
  • 불필요한 콘솔 로그나 주석을 제거했나요?

Summary by CodeRabbit

  • New Features
    • Added profile navigation – click on commenter names to view user profiles
    • Enhanced comment management interface with refined editing and action menus

@shinwokkang shinwokkang self-assigned this Apr 22, 2026
@vercel

vercel Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
check-mo Ready Ready Preview, Comment Apr 24, 2026 8:35am
checkmo Ready Ready Preview, Comment Apr 24, 2026 8:35am

@coderabbitai

coderabbitai Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4606f62a-e47c-4ad1-8565-2f2b550cf0b9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/264-my-comment-ham

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

❤️ Share

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

@shinwokkang

Copy link
Copy Markdown
Contributor Author

@coderabbitai 여기에서 파일이 너무 긴데, 어떻게 리팩토링을 진행하면 될까요?

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll fetch the PR details to identify which file you're referring to.


🏁 Script executed:

#!/bin/bash
# Get PR details and files changed
gh pr view 265 --repo checkmo2025/FE --json files,title,body

echo "---FILES CHANGED---"
gh pr diff 265 --repo checkmo2025/FE --name-only

Length of output: 799


🏁 Script executed:

#!/bin/bash
# Read the comment_item.tsx file
fd "comment_item.tsx" --exec cat {}

Length of output: 8931


@shinwokkang comment_item.tsx를 분석했습니다. 현재 약 230줄로, 크게 4가지 단위로 분리하면 깔끔하게 리팩토링할 수 있습니다.


📁 추천 파일 구조

src/components/base-ui/Comment/
├── comment_item.tsx          # 메인 컴포넌트 (슬림해짐)
├── CommentHeader.tsx         # 프로필 + 이름 + 작성자 뱃지 + 날짜
├── CommentMenu.tsx           # 드롭다운 메뉴 (답글/수정/삭제/신고)
├── CommentEditForm.tsx       # 수정 모드 textarea + 버튼
└── hooks/
    └── useClickOutside.ts    # 바깥 클릭 감지 훅

또는 formatDate는 공용 유틸이라면:

src/utils/formatDate.ts

✂️ 분리 상세 안

1. useClickOutside.ts — 공용 훅 추출

// src/components/base-ui/Comment/hooks/useClickOutside.ts
import { useEffect, RefObject } from "react";

export function useClickOutside(ref: RefObject<HTMLElement>, handler: () => void) {
  useEffect(() => {
    const handleClick = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) {
        handler();
      }
    };
    document.addEventListener("mousedown", handleClick);
    return () => document.removeEventListener("mousedown", handleClick);
  }, [ref, handler]);
}

2. CommentHeader.tsx — 상단 프로필/날짜 영역

// src/components/base-ui/Comment/CommentHeader.tsx
import Image from "next/image";

type CommentHeaderProps = {
  authorName: string;
  profileImgSrc: string;
  isAuthor: boolean;
  createdAt: string;
};

function formatDate(iso: string) {
  const date = new Date(iso);
  return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, "0")}.${String(date.getDate()).padStart(2, "0")}`;
}

export default function CommentHeader({ authorName, profileImgSrc, isAuthor, createdAt }: CommentHeaderProps) {
  return (
    <div className="flex items-center justify-between">
      <div className="flex items-center gap-2">
        <div className="relative w-8 h-8 shrink-0 rounded-full overflow-hidden">
          <Image src={profileImgSrc} alt={authorName} fill className="object-cover" sizes="32px" />
        </div>
        <span className="Subhead_4_1 text-Gray-7">{authorName}</span>
        {isAuthor && <span className="px-2 py-0.5 Subhead_4 text-Gray-3">작성자</span>}
      </div>
      <span className="Body_1_2 text-Gray-3">{formatDate(createdAt)}</span>
    </div>
  );
}

3. CommentEditForm.tsx — 수정 모드 UI

// src/components/base-ui/Comment/CommentEditForm.tsx
type CommentEditFormProps = {
  value: string;
  onChange: (v: string) => void;
  onSave: () => void;
  onCancel: () => void;
};

export default function CommentEditForm({ value, onChange, onSave, onCancel }: CommentEditFormProps) {
  return (
    <div className="flex flex-col gap-2 mt-2">
      <textarea
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className="w-full min-h-[80px] px-4 py-3 rounded-lg border border-Subbrown-4 bg-White Body_1_2 text-Gray-7 outline-none focus:border-primary-3 resize-none whitespace-pre-wrap"
        autoFocus
      />
      <div className="flex justify-end gap-2">
        <button type="button" onClick={onCancel} className="px-4 py-2 rounded-lg bg-Gray-2 text-Gray-6 subhead_4_1">취소</button>
        <button type="button" onClick={onSave} className="px-4 py-2 rounded-lg bg-primary-3 text-White subhead_4_1">저장</button>
      </div>
    </div>
  );
}

4. CommentMenu.tsx — 드롭다운 메뉴 (가장 복잡한 부분)

// src/components/base-ui/Comment/CommentMenu.tsx
import Image from "next/image";
import { useRef } from "react";
import { useClickOutside } from "./hooks/useClickOutside";

type CommentMenuProps = {
  open: boolean;
  onToggle: () => void;
  onClose: () => void;
  hasReplyAction: boolean;
  canEdit: boolean;
  canDelete: boolean;
  canReport: boolean;
  onReply: () => void;
  onEdit: () => void;
  onDelete: () => void;
  onReport: () => void;
};

export default function CommentMenu({
  open, onToggle, onClose,
  hasReplyAction, canEdit, canDelete, canReport,
  onReply, onEdit, onDelete, onReport,
}: CommentMenuProps) {
  const menuRef = useRef<HTMLDivElement>(null);
  useClickOutside(menuRef, onClose);

  const hasManageAction = canEdit || canDelete;

  return (
    <div className="relative shrink-0" ref={menuRef}>
      <button type="button" onClick={onToggle} className="p-1 cursor-pointer">
        <Image src="/menu_dots.svg" alt="메뉴" width={20} height={20} />
      </button>
      {open && (
        <div className="absolute right-0 top-full mt-1 w-[137px] rounded-lg bg-White shadow-lg z-10 overflow-hidden">
          {hasReplyAction && (
            <>
              <button type="button" onClick={() => { onReply(); onClose(); }}
                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="/reply2.svg" alt="답글" width={24} height={24} />
                답글달기
              </button>
              {(hasManageAction || canReport) && <div className="mx-2 border-b border-Subbrown-4" />}
            </>
          )}
          {hasManageAction ? (
            <>
              {canDelete && (
                <button type="button" onClick={() => { onDelete(); onClose(); }}
                  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="/delete.svg" alt="삭제" width={24} height={24} />
                  삭제하기
                </button>
              )}
              {canEdit && canDelete && <div className="mx-2 border-b border-Subbrown-4" />}
              {canEdit && (
                <button type="button" onClick={() => { onEdit(); onClose(); }}
                  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="/edit2.svg" alt="수정" width={24} height={24} />
                  수정하기
                </button>
              )}
            </>
          ) : (
            canReport && (
              <button type="button" onClick={() => { onReport(); onClose(); }}
                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="/report.svg" alt="신고" width={24} height={24} />
                신고하기
              </button>
            )
          )}
        </div>
      )}
    </div>
  );
}

5. comment_item.tsx — 분리 후 메인 컴포넌트 (슬림해진 모습)

"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import CommentHeader from "./CommentHeader";
import CommentMenu from "./CommentMenu";
import CommentEditForm from "./CommentEditForm";

// ... (타입 동일)

export default function CommentItem({ id, authorName, profileImgSrc = "/profile2.svg",
  content, createdAt, isAuthor = false, isMine = false, isReply = false,
  canEdit = false, canDelete = false, canReport = false,
  onReply, onEdit, onDelete, onReport,
}: CommentItemProps) {
  const [menuOpen, setMenuOpen] = useState(false);
  const [isEditing, setIsEditing] = useState(false);
  const [editContent, setEditContent] = useState(content);

  useEffect(() => { setEditContent(content); }, [content]);

  const handleSaveEdit = () => {
    if (editContent.trim() && editContent !== content) onEdit?.(id, editContent);
    setIsEditing(false);
  };

  const derivedCanEdit = canEdit || isMine;
  const derivedCanDelete = canDelete || isMine;
  const derivedCanReport = canReport || !isMine;
  const hasReplyAction = !isReply && !!onReply;
  const hasMenuAction = hasReplyAction || derivedCanDelete || derivedCanEdit || derivedCanReport;

  const body = (
    <div className="flex flex-col gap-2 flex-1">
      <CommentHeader authorName={authorName} profileImgSrc={profileImgSrc}
        isAuthor={isAuthor} createdAt={createdAt} />
      {!isEditing ? (
        <div className="flex items-start justify-between gap-2">
          <p className="Body_1_2 text-Gray-5 flex-1 whitespace-pre-wrap">{content}</p>
          {hasMenuAction && (
            <CommentMenu
              open={menuOpen} onToggle={() => setMenuOpen((v) => !v)} onClose={() => setMenuOpen(false)}
              hasReplyAction={hasReplyAction}
              canEdit={derivedCanEdit} canDelete={derivedCanDelete} canReport={derivedCanReport}
              onReply={() => onReply?.(id)}
              onEdit={() => setIsEditing(true)}
              onDelete={() => onDelete?.(id)}
              onReport={() => onReport?.(id)}
            />
          )}
        </div>
      ) : (
        <CommentEditForm value={editContent} onChange={setEditContent}
          onSave={handleSaveEdit} onCancel={() => { setEditContent(content); setIsEditing(false); }} />
      )}
    </div>
  );

  if (isReply) {
    return (
      <div className="flex items-center gap-4 t:gap-8 py-4">
        <Image src="/reply.svg" alt="대댓글" width={40} height={40}
          className="shrink-0 w-6 h-6 t:w-10 t:h-10 self-start mt-1" />
        {body}
      </div>
    );
  }
  return <div className="py-4">{body}</div>;
}

🔑 핵심 요약

분리 대상 이유
useClickOutside 여러 컴포넌트에서 재사용 가능한 범용 훅
CommentHeader 프로필/날짜 UI가 독립적으로 테스트·수정 가능
CommentMenu 가장 복잡한 JSX 블록, 드롭다운 상태도 함께 내부화
CommentEditForm 수정 UI가 독립적인 책임 단위

이렇게 분리하면 comment_item.tsx가 ~230줄 → ~70줄 수준으로 줄어들고, 각 컴포넌트를 독립적으로 유지보수할 수 있습니다.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 render NaN.NaN.NaN since the Date methods return NaN and padStart keeps the 3-char string. If upstream can pass bad values, consider returning an empty string (or the original) when isNaN(date.getTime()). Also, an explicit : string return 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 stabilizing handler and handling touch events.

Two small improvements:

  1. Including handler in 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 keep handler in a ref so the effect can depend only on ref:

    ♻️ 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]);
     }
  2. Adding touchstart improves mobile behavior for dropdowns that close on outside tap (consistent with common libraries like Headless UI / Radix).

Both are optional if consumers always memoize handler and 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 저장 when value.trim() is empty to avoid sending empty updates (assuming the parent doesn't already prevent that).
  • Consider Esc → onCancel, Ctrl/Cmd+Enter → onSave for 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, onClick that calls handler + onClose, <Image> icon, label). A small MenuItem sub-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 adding role="menu" to the dropdown and role="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: canReport is silently suppressed whenever hasManageAction is true.

The hasManageAction ? (...) : (canReport && ...) branch makes "신고하기" unreachable if either canEdit or canDelete is true — even when canReport is also true. In the current CommentItem this is masked because derivedCanReport is !isMine (opposite of isMine), but the component contract is stricter than its implementation suggests. Either:

  • Document this mutual exclusivity in CommentMenuProps (e.g., note that canReport is ignored when canEdit || 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

📥 Commits

Reviewing files that changed from the base of the PR and between 81a6f46 and 0e8844a.

📒 Files selected for processing (11)
  • src/components/base-ui/Comment/comment_edit_form.tsx
  • src/components/base-ui/Comment/comment_header.tsx
  • src/components/base-ui/Comment/comment_item.tsx
  • src/components/base-ui/Comment/comment_list.tsx
  • src/components/base-ui/Comment/comment_list_notice.tsx
  • src/components/base-ui/Comment/comment_menu.tsx
  • src/components/base-ui/Comment/comment_section.tsx
  • src/components/base-ui/Comment/comment_section_bookcase.tsx
  • src/components/base-ui/Comment/comment_section_notice.tsx
  • src/hooks/useClickOutside.ts
  • src/utils/date.ts
💤 Files with no reviewable changes (1)
  • src/components/base-ui/Comment/comment_section.tsx

Comment on lines +4 to +10
type CommentHeaderProps = {
authorName: string;
profileImgSrc: string;
isAuthor: boolean;
createdAt: string;
onProfileClick?: (nickname: string) => void;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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=tsx

Repository: 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 2

Repository: 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 -50

Repository: 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 -80

Repository: 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.

Comment on lines +20 to +42
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines 50 to 52
useEffect(() => {
setEditContent(content);
}, [content]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +61 to +63
const derivedCanEdit = canEdit || isMine;
const derivedCanDelete = canDelete || isMine;
const derivedCanReport = canReport || !isMine;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +39 to +45
<button
type="button"
onClick={onToggle}
className="p-1 cursor-pointer"
>
<Image src="/menu_dots.svg" alt="메뉴" width={20} height={20} />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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}`)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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}`)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@shinwokkang
shinwokkang merged commit 00cc75b into dev Apr 24, 2026
4 checks passed
@shinwokkang
shinwokkang deleted the fix/264-my-comment-ham branch April 24, 2026 09:34

This branch was successfully deployed

2 active deployments
Preview – check-mo — 0e8844aa Deployed Apr 24, 2026 by vercel[bot]
Preview – checkmo — 0e8844aa Deployed Apr 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant