Skip to content

Feat(client): card edit delete#83

Merged
jjangminii merged 4 commits intodevelopfrom
feat/#81/card-edit-delete
Sep 13, 2025
Merged

Feat(client): card edit delete#83
jjangminii merged 4 commits intodevelopfrom
feat/#81/card-edit-delete

Conversation

@jjangminii
Copy link
Collaborator

@jjangminii jjangminii commented Sep 12, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  • 리마인드/북마크에 디테일 버튼 클릭시 옵셩 버튼 활성화
  • 수정하기 버튼 클릭시 모달 활성화

⭐ PR Point (To Reviewer)

  • 카드/아이콘 주변에 메뉴를 띄울 때 컨테이너의 overflow / position / z-index 영향으로 relative 기준 배치가 어긋나거나 가려지는 문제가 있었습니다. 그래서 메뉴를 Portal로 띄었습니다-!

📷 Screenshot

2025-09-12.9.34.41.mov

Summary by CodeRabbit

  • New Features

    • 카드별 앵커형 옵션 메뉴 추가: 각 카드의 옵션 버튼에서 편집/삭제 등 동작을 카드 근처에 표시하고 선택 시 자동으로 닫힘.
    • 전체 화면 편집 모달 추가: 옵션의 편집 선택 시 즉시 수정 가능한 풀스크린 모달로 편집하고 배경 클릭 또는 닫기로 닫음.
    • 배지 필터(전체 / 안 읽음) 추가 및 카운트 표시로 빠른 목록 필터링 지원.
  • Style

    • 레이아웃·여백 조정 및 메뉴/모달에 맞춘 구조 개선으로 가독성과 사용성 향상.

@jjangminii jjangminii self-assigned this Sep 12, 2025
@jjangminii jjangminii linked an issue Sep 12, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

Walkthrough

MyBookmark와 Remind 페이지에 카드별 앵커드 옵션 메뉴와 카드 편집 모달을 추가했다. 디자인 시스템의 Card/RemindCard/MyBookmarkCard에 onClick/onOptionsClick props가 도입되었고, 메뉴 위치 계산용 belowOf 유틸이 새로 추가되었다.

Changes

Cohort / File(s) Summary of changes
Pages: Anchored menu + Edit modal
apps/client/src/pages/myBookmark/MyBookmark.tsx, apps/client/src/pages/remind/Remind.tsx
카드 옵션 버튼에 연결된 useAnchoredMenu 도입, OptionsMenuPortal 렌더링, id 기반 타이틀 조회 헬퍼 추가, Edit 선택 시 CardEditModal 오버레이 오픈/닫기 로직 추가, 상태(activeBadge, isEditOpen) 및 핸들러 추가, 레이아웃/스페이싱 일부 조정.
Design System: Card props 확장
packages/design-system/src/components/card/Card.tsx, .../RemindCard.tsx, .../MyBookmarkCard.tsx
공통 BasePropsonClick?: () => void, onOptionsClick?: (e: React.MouseEvent<HTMLButtonElement>) => void 추가. RemindCard/MyBookmarkCard의 옵션 버튼에서 onOptionsClick 호출로 이벤트 전달.
Utils: Anchor positioning
apps/client/src/shared/utils/anchorPosition.ts
belowOf(anchor, gap = 8) 유틸 추가(앵커 요소 바로 아래에 메뉴를 배치하기 위한 좌표 계산). 기존 rightOf는 변경 없음.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as Card (Remind/MyBookmark)
  participant P as Page 컴포넌트
  participant H as useAnchoredMenu
  participant M as OptionsMenuPortal
  participant E as CardEditModal

  U->>C: 옵션 버튼 클릭
  C-->>P: onOptionsClick(e)
  P->>H: openMenu({ id, anchor: e.currentTarget, pos: belowOf })
  H-->>P: menu state (open, top,left,context)
  P->>M: 렌더(제목, id, onEdit, onDelete, onClose)

  alt Edit 선택
    U->>M: Edit 클릭
    M-->>P: onEdit()
    P->>H: closeMenu()
    P->>E: isEditOpen = true (모달 오픈)
    U->>E: 저장/닫기
    E-->>P: onClose()
    P->>E: isEditOpen = false
  else Delete 선택
    U->>M: Delete 클릭
    M-->>P: onDelete()
    P->>H: closeMenu()
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

frontend

Suggested reviewers

  • constantly-dev
  • jllee000
  • karnelll

Poem

툭, 버튼 찰칵—메뉴가 내려와요
아래로 살짝, 자리 찾아 착착 🐰
편집창 반짝, 닫으면 사르르
삭제는 조심, 로그로 찍고 닫네
깡총깡총 토끼가 축하 춤을 춰요 ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning PR은 이슈 #81(카드 수정/삭제 연결)의 UI 측면 대부분을 구현하여 옵션 메뉴와 편집 모달을 도입했으나 삭제 동작은 단순 로그 출력으로 남아 있어 '삭제 기능 연결'이라는 목적을 완전하게 달성하지 못했습니다. 이슈 #25(디자인 시스템의 Progress 컴포넌트 구현)는 요구사항과 무관하며 해당 기능 구현 코드가 포함되어 있지 않습니다. 따라서 현재 PR은 링크된 모든 이슈의 완료 요건을 충족한다고 보기 어렵습니다. 해결 권고: 우선 PR에 #25가 포함된 의도가 맞는지 확인하고, 의도하지 않았다면 linked_issues에서 #25를 제거하거나 PR 설명에 이유를 명확히 적으세요. 이슈 #81 완전 이행을 위해서는 삭제 API 연동 및 편집 모달에서의 변경 사항 저장(서버 반영) 구현과 관련 단위/통합 검증을 추가해야 합니다. 구현 완료 후 PR 설명과 커밋에 변경 범위와 수용 기준을 명시한 뒤 재검토를 요청하세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed 현재 PR 제목 "Feat(client): card edit delete"은 PR의 핵심 변경사항인 카드 편집·삭제 기능 연결을 간결하게 요약하고 있어 변경 목적과 관련성이 높습니다. 제목은 짧고 한 문장 형태로 되어 있어 히스토리 열람 시 주요 의도를 빠르게 파악할 수 있습니다. 다만 영어 단어만 공백으로 이어쓴 형태라 'edit/delete'처럼 구분자나 한글 표기를 사용하면 가독성이 더 좋아집니다.
Out of Scope Changes Check ✅ Passed 검토된 변경사항은 카드 옵션 메뉴와 편집 모달, 포탈 배치, 그리고 카드 컴포넌트의 옵션 핸들러와 아래 배치(belowOf) 유틸 추가 등 기능 구현 목적과 일치하는 범위에 한정되어 있습니다. 디자인 시스템의 카드 API 변경(onClick/onOptionsClick 추가)은 해당 기능을 위해 합리적인 수정으로 보이며 다른 모듈에 불필요한 변경은 보이지 않습니다. 따라서 명백한 범위 외 변경사항은 발견되지 않았습니다.
Description Check ✅ Passed 현재 PR 설명은 템플릿의 필수 섹션(관련 이슈, 작업 내용, PR 포인트, 스크린샷)을 포함하고 있으며 close #81이 명시되어 있어 템플릿 요구사항을 충족합니다. 작업 요약과 포인트(포탈 사용 이유)도 기술되어 있어 리뷰어가 구현 의도를 이해하기에 충분합니다. 다만 삭제 동작이 플레이스홀더인지와 편집 결과의 적용 범위(로컬 상태/서버 반영)를 명시하면 수용 기준이 더 명확해집니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#81/card-edit-delete

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

@github-actions github-actions bot added the feat 기능 개발하라 개발 달려라 달려 label Sep 12, 2025
@github-actions
Copy link

github-actions bot commented Sep 12, 2025

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/design-system/src/components/card/RemindCard.tsx (1)

48-55: 옵션 버튼 클릭 시 카드 onClick 동시 실행 가능성 → 이벤트 전파 차단 필요

옵션 버튼 클릭이 상위(Card/컨테이너)의 onClick까지 전파될 수 있어 UX가 깨질 수 있습니다. 전파를 명시적으로 막아주세요. 또한 aria-label은 실제 동작에 맞게 수정 권장.

-          <button
+          <button
             type="button"
-            aria-label="카테고리 상세"
+            aria-label="옵션 메뉴 열기"
             className="cursor-pointer self-start"
-            onClick={(e) => onOptionsClick?.(e)}
+            onClick={(e) => {
+              e.stopPropagation();
+              onOptionsClick?.(e);
+            }}
           >
packages/design-system/src/components/card/MyBookmarkCard.tsx (1)

40-47: 옵션 버튼 클릭 시 상위 onClick 전파 차단 필요 + 라벨 정정

RemindCard와 동일 이슈입니다. 전파 차단 및 접근성 라벨 보정 제안.

-          <button
+          <button
             type="button"
-            aria-label="카테고리 상세"
+            aria-label="옵션 메뉴 열기"
             className="cursor-pointer self-start"
-            onClick={(e) => onOptionsClick?.(e)}
+            onClick={(e) => {
+              e.stopPropagation();
+              onOptionsClick?.(e);
+            }}
           >
🧹 Nitpick comments (16)
apps/client/src/shared/utils/anchorPosition.ts (1)

6-9: belowOf 구현은 적절. 다만 뷰포트 충돌/클리핑 대응 추가 권장

현재는 우하단으로만 배치되어 우/하단 가장자리에서 메뉴가 화면 밖으로 벗어날 수 있습니다. 추후 flip(위/아래 전환) + clamp(좌/우 경계 보정)를 넣거나 Floating UI 같은 라이브러리로 대체를 고려해주세요.

예시(간단 보정 타입 추가만):

+export type AnchorPos = { top: number; left: number };
-export const belowOf = (anchor: HTMLElement, gap = 8) => {
+export const belowOf = (anchor: HTMLElement, gap = 8): AnchorPos => {
   const r = anchor.getBoundingClientRect();
   return { top: r.bottom + gap, left: r.left };
 };
packages/design-system/src/components/card/RemindCard.tsx (2)

34-36: 이미지 대체 텍스트 누락

동적 이미지에 alt가 없어 스크린리더 접근성이 떨어집니다. 제목 기반 혹은 빈 alt라도 지정해주세요.

-          <img src={imageUrl} className="h-full w-full object-cover" />
+          <img src={imageUrl} alt={title || ''} className="h-full w-full object-cover" />

11-13: onClick Prop 선언만 있고 미사용

현재 카드 자체 클릭 동작이 연결되어 있지 않습니다. 의도라면 타입에서 제거, 사용할 예정이라면 루트 컨테이너(BaseCard 등)에 전달해주세요.

-const RemindCard = ({
+const RemindCard = ({
   title,
   content,
   category,
   imageUrl,
   timeRemaining,
-  onOptionsClick,
+  onOptionsClick,
+  onClick,
 }: RemindCardProps) => {
   return (
-    <BaseCard>
+    <BaseCard onClick={onClick}>

Also applies to: 21-22

packages/design-system/src/components/card/Card.tsx (2)

27-31: onOptionsClick 타입 중복 선언 제거

BaseProps에 이미 존재하므로 함수 시그니처에서 재정의는 불필요합니다.

-const Card = (
-  props: CardProps & {
-    onOptionsClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
-  }
-) => {
+const Card = (props: CardProps) => {

36-42: 중복 전달 제거

{...props}onOptionsClick이 포함되어 있어 명시적 재전달은 중복입니다.

-      {type === 'remind' && (
-        <RemindCard {...props} onOptionsClick={onOptionsClick} />
-      )}
+      {type === 'remind' && <RemindCard {...props} />}
 
-      {type === 'bookmark' && (
-        <MyBookmarkCard {...props} onOptionsClick={onOptionsClick} />
-      )}
+      {type === 'bookmark' && <MyBookmarkCard {...props} />}
packages/design-system/src/components/card/MyBookmarkCard.tsx (2)

26-28: 이미지 대체 텍스트 누락

동적 이미지 alt 추가를 권장합니다.

-          <img src={imageUrl} className="h-full w-full object-cover" />
+          <img src={imageUrl} alt={title || ''} className="h-full w-full object-cover" />

11-13: onClick Prop 미사용

의도 확인 필요. 사용할 계획이라면 루트 컨테이너에 전달하세요.

-const MyBookmarkCard = ({
+const MyBookmarkCard = ({
   title,
   content,
   category,
   imageUrl,
   date,
-  onOptionsClick,
+  onOptionsClick,
+  onClick,
 }: MyBookmarkCardProps) => {
   return (
-    <BaseCard>
+    <BaseCard onClick={onClick}>

Also applies to: 21-22

apps/client/src/pages/myBookmark/MyBookmark.tsx (4)

52-54: 의미 없는 카드 onClick 전달 제거

빈 핸들러는 유지 비용만 늘립니다. 필요 시점에 추가하는 편이 낫습니다.

-            onClick={() => {}}
             onOptionsClick={(e) => openMenu(data.id, e.currentTarget)}

64-71: 메뉴 닫기 이중 호출 제거

OptionsMenuPortal 내부에서 onClose를 이미 호출합니다. 중복 close는 불필요합니다.

-          onEdit={() => {
-            setIsEditOpen(true);
-            closeMenu();
-          }}
-          onDelete={(id) => {
-            console.log('delete', id);
-            closeMenu();
-          }}
+          onEdit={() => setIsEditOpen(true)}
+          onDelete={(id) => {
+            console.log('delete', id);
+          }}

75-86: 모달 z-index가 메뉴(10000)보다 낮음

메뉴가 우선 렌더되면 레이어링 깜빡임이 발생할 수 있습니다. 모달 z를 더 높게 가져가세요.

-      {isEditOpen && (
-        <div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog">
+      {isEditOpen && (
+        <div className="fixed inset-0 z-[11000]" aria-modal="true" role="dialog">

추가로 ESC 닫기, 포커스 트랩은 추후 접근성 보강 시 고려 바랍니다.


2-2: 데이터 소스 확인 요청: REMIND_MOCK_DATA 사용

북마크 페이지에서 리마인드 목업을 사용하고 있습니다. 의도된 임시 연결인지, 별도 북마크 데이터 소스로 교체해야 하는지 확인이 필요합니다.

원하시면 북마크 전용 목업/타입 분리를 위한 작은 PR도 도와드릴게요.

Also applies to: 21-22

apps/client/src/pages/remind/Remind.tsx (5)

13-19: 앵커 메뉴 UX 보완: ESC 닫기·경계 충돌 처리·rAF 스로틀 제안

현재 훅은 스크롤/리사이즈/바깥클릭만 처리합니다. 다음을 추가하면 견고해집니다.

  • Escape로 닫기(keydown)
  • 메뉴가 뷰포트 밖으로 나갈 때 좌표 클램프/플립
  • 스크롤 중 연속 reflow를 rAF로 스로틀

예시(훅 수정안):

// useAnchoredMenu.ts 일부
const rafRef = useRef<number | null>(null);

useEffect(() => {
  if (!state.open) return;

  const syncPos = () => {
    if (rafRef.current != null) return;
    rafRef.current = requestAnimationFrame(() => {
      rafRef.current = null;
      if (!state.anchorEl) return;

      const base = getPos(state.anchorEl);
      const menuEl = containerRef.current;
      if (!menuEl) {
        setState((s) => ({ ...s, pos: base }));
        return;
      }
      const w = menuEl.offsetWidth;
      const h = menuEl.offsetHeight;
      const left = Math.max(8, Math.min(window.innerWidth - w - 8, base.left));
      const top = Math.max(8, Math.min(window.innerHeight - h - 8, base.top));
      setState((s) => ({ ...s, pos: { top, left } }));
    });
  };

  const onKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'Escape') close();
  };

  window.addEventListener('scroll', syncPos, true);
  window.addEventListener('resize', syncPos);
  document.addEventListener('keydown', onKeyDown);
  // ...
  return () => {
    window.removeEventListener('scroll', syncPos, true);
    window.removeEventListener('resize', syncPos);
    document.removeEventListener('keydown', onKeyDown);
    if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
  };
}, [state.open, state.anchorEl, close, getPos]);

원하시면 훅/유틸에 위 개선을 반영해 별 PR로 올려드릴게요.


21-22: getItemTitle는 매 렌더 O(n) 탐색 → Map 캐시로 O(1)로 개선

카드 수가 늘면 find 비용이 누적됩니다. Map을 메모이즈해 상수시간 조회로 바꾸는 것을 권장합니다.

적용 diff(해당 범위):

-  const getItemTitle = (id: number | null) =>
-    id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
+  const getItemTitle = (id: number | null) =>
+    id == null ? '' : (titleById.get(id) ?? '');

추가 코드(파일 상단 import, 그리고 컴포넌트 내부 어디든 1회 정의):

// import { useMemo, useState } from 'react';

const titleById = useMemo(
  () => new Map(REMIND_MOCK_DATA.map((d) => [d.id, d.title] as const)),
  []
);

51-51: 옵션 버튼 클릭 버블링/타입 확인

Card 자체 onClick이 있다면 옵션 버튼 클릭이 버블링되어 카드 클릭이 함께 실행될 수 있습니다. 내부 버튼에서 e.stopPropagation() 처리되어 있는지 확인 부탁드립니다. 또한 명시적으로 타입을 적어 가독성을 높일 수 있습니다.

onOptionsClick={(e: React.MouseEvent<HTMLElement>) =>
  openMenu(data.id, e.currentTarget as HTMLElement)
}

55-70: 메뉴 콜백 시그니처/순서, 스타일 전달 간소화 + z-index 정책 제안

  • style ?? undefined는 중복입니다.
  • onEdit에서 메뉴를 먼저 닫고 모달을 여는 순서가 플리커를 줄입니다.
  • Portal z-index(10000)와 모달 오버레이(1000)가 충돌할 수 있으니 계층 정책을 정수 단위 토큰으로 통일하는 것을 추천합니다.

적용 diff:

         <OptionsMenuPortal
           open={menu.open}
-          style={style ?? undefined}
+          style={style}
           containerRef={containerRef}
           categoryId={menu.categoryId}
           getCategoryName={getItemTitle}
-          onEdit={() => {
-            setIsEditOpen(true);
-            closeMenu();
-          }}
-          onDelete={(id) => {
+          onEdit={(id, _name) => {
+            closeMenu();
+            setIsEditOpen(true);
+          }}
+          onDelete={(id, _name) => {
             console.log('delete', id);
             closeMenu();
           }}
           onClose={closeMenu}
         />

추가로, z-index를 design token(e.g., --z-menu, --z-modal)로 관리하면 충돌 예방에 좋습니다.


73-83: 중복 dialog 역할 제거·ESC 닫기·모달이 메뉴보다 위에 렌더되도록 조정

외부 래퍼와 CardEditModal 모두 role="dialog"/aria-modal="true"를 가지면 스크린 리더 혼란이 생길 수 있습니다. 래퍼의 역할은 제거하고, ESC 닫기 및 z-index를 메뉴(10000)보다 높게 올리는 것을 권장합니다.

적용 diff:

-      {isEditOpen && (
-        <div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog">
+      {isEditOpen && (
+        <div
+          className="fixed inset-0 z-[11000]"
+          tabIndex={-1}
+          onKeyDown={(e) => {
+            if (e.key === 'Escape') setIsEditOpen(false);
+          }}
+        >
           <div
             className="absolute inset-0 bg-black/60 backdrop-blur-[2px]"
             onClick={() => setIsEditOpen(false)}
           />
           <div className="absolute inset-0 flex items-center justify-center p-4">
             <CardEditModal onClose={() => setIsEditOpen(false)} />
           </div>
         </div>
       )}

보완사항(옵션):

  • 모달 오픈 시 body 스크롤 잠금
// 컴포넌트 내
useEffect(() => {
  if (!isEditOpen) return;
  const prev = document.body.style.overflow;
  document.body.style.overflow = 'hidden';
  return () => { document.body.style.overflow = prev; };
}, [isEditOpen]);
  • 포커스 트랩/초점 복귀는 디자인시스템 Modal 컴포넌트가 있다면 그걸 사용하는 것이 가장 안전합니다.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e981d92 and e902ebd.

📒 Files selected for processing (6)
  • apps/client/src/pages/myBookmark/MyBookmark.tsx (2 hunks)
  • apps/client/src/pages/remind/Remind.tsx (2 hunks)
  • apps/client/src/shared/utils/anchorPosition.ts (1 hunks)
  • packages/design-system/src/components/card/Card.tsx (2 hunks)
  • packages/design-system/src/components/card/MyBookmarkCard.tsx (3 hunks)
  • packages/design-system/src/components/card/RemindCard.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/client/src/pages/myBookmark/MyBookmark.tsx (5)
apps/client/src/shared/hooks/useAnchoredMenu.ts (1)
  • useAnchoredMenu (13-86)
apps/client/src/shared/utils/anchorPosition.ts (1)
  • belowOf (6-9)
apps/client/src/pages/remind/constants/index.ts (1)
  • REMIND_MOCK_DATA (1-114)
apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx (1)
  • OptionsMenuPortal (15-45)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
  • CardEditModal (22-169)
apps/client/src/pages/remind/Remind.tsx (4)
apps/client/src/shared/hooks/useAnchoredMenu.ts (1)
  • useAnchoredMenu (13-86)
apps/client/src/shared/utils/anchorPosition.ts (1)
  • belowOf (6-9)
apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx (1)
  • OptionsMenuPortal (15-45)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
  • CardEditModal (22-169)
🔇 Additional comments (4)
apps/client/src/pages/remind/Remind.tsx (4)

2-6: 새 Portal/앵커 메뉴 도입 방향성 좋습니다

디자인시스템/Portal/훅 조합으로 레이아웃 이슈를 분리한 점 좋습니다. 임포트 구성도 문제 없어 보입니다.


10-11: 배지 상태를 유니온 타입으로 고정한 점 👍

의도 명확하고 오타/분기 실수를 줄여줍니다. 모달 상태도 단순합니다.


31-31: 배지 토글만 있고 목록 필터링/카운트 동기화는 없음 — 의도 확인 요청

현재 activeBadge는 UI 하이라이트만 변경합니다. 실제 리스트 필터/카운트 연동이 스코프 외면 OK, 아니라면 데이터 필터링과 countNum 일치 여부 확인 부탁드립니다.

Also applies to: 37-37


42-42: 카드 그리드 스크롤 컨테이너 구성 적절

Portal 고정 포지셔닝과 캡처 스크롤 리스너 조합과 잘 맞습니다.

Copy link
Member

@constantly-dev constantly-dev left a comment

Choose a reason for hiding this comment

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

수고하셨습니다 이제 API 연결만 하면 되겠네요!!!
조금만 더 화이팅~~ 👍

content={data.content}
category={data.category}
date="2024.08.15"
onClick={() => {}}
Copy link
Member

Choose a reason for hiding this comment

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

여기는 이후에 동작이 추가되는거죠??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵넵

Comment on lines +11 to +12
onClick?: () => void;
onOptionsClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
Copy link
Member

Choose a reason for hiding this comment

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

제가 해당 옵션 클릭 인터페이스를 놓쳤네요..감사합니다!!
onClick은 전체 카드를 누를 때 실행되는 건가요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵넵-!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

41-52: 안 읽음 배지 카운트 소스 변경 필요

MyBookmark.tsx에서 "안 읽음" 배지가 readArticles?.totalUnreadArticle을 참조하고 있으므로 unreadArticles?.totalUnreadArticle로 교체하세요.

-          countNum={readArticles?.totalUnreadArticle || 0}
+          countNum={unreadArticles?.totalUnreadArticle || 0}
♻️ Duplicate comments (1)
apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

65-69: 빈 onClick 핸들러 제거 또는 TODO 명시

빈 함수는 불필요한 할당을 유발합니다. 필요 없다면 prop 자체를 제거하거나 TODO로 명확히 남겨주세요.

-              onClick={() => {}}
🧹 Nitpick comments (2)
apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

88-103: 작은 정리: style ?? undefined 제거 및 onEdit 실행 순서 조정 권장

  • style ?? undefined는 중복입니다.
  • 모달 열 때 메뉴를 먼저 닫으면 z-index 간 레이스를 줄일 수 있습니다.
-          style={style ?? undefined}
+          style={style}
@@
-          onEdit={() => {
-            setIsEditOpen(true);
-            closeMenu();
-          }}
+          onEdit={() => {
+            closeMenu();
+            setIsEditOpen(true);
+          }}
apps/client/src/pages/remind/Remind.tsx (1)

72-87: 작은 정리: style ?? undefined 제거 및 onEdit 순서 조정 권장

MyBookmark와 동일한 사유입니다.

-          style={style ?? undefined}
+          style={style}
@@
-          onEdit={() => {
-            setIsEditOpen(true);
-            closeMenu();
-          }}
+          onEdit={() => {
+            closeMenu();
+            setIsEditOpen(true);
+          }}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e902ebd and e130a2c.

📒 Files selected for processing (2)
  • apps/client/src/pages/myBookmark/MyBookmark.tsx (4 hunks)
  • apps/client/src/pages/remind/Remind.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/client/src/pages/myBookmark/MyBookmark.tsx (5)
apps/client/src/shared/hooks/useAnchoredMenu.ts (1)
  • useAnchoredMenu (13-86)
apps/client/src/shared/utils/anchorPosition.ts (1)
  • belowOf (6-9)
apps/client/src/pages/remind/constants/index.ts (1)
  • REMIND_MOCK_DATA (1-114)
apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx (1)
  • OptionsMenuPortal (15-45)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
  • CardEditModal (22-169)
apps/client/src/pages/remind/Remind.tsx (5)
apps/client/src/shared/hooks/useAnchoredMenu.ts (1)
  • useAnchoredMenu (13-86)
apps/client/src/shared/utils/anchorPosition.ts (1)
  • belowOf (6-9)
apps/client/src/pages/remind/constants/index.ts (1)
  • REMIND_MOCK_DATA (1-114)
apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx (1)
  • OptionsMenuPortal (15-45)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
  • CardEditModal (22-169)
🔇 Additional comments (4)
apps/client/src/pages/myBookmark/MyBookmark.tsx (2)

17-23: 앵커드 메뉴 훅 연동 방식 깔끔합니다.

Portal + fixed 좌표로 스크롤/리사이즈 대응까지 정리된 패턴이라 재사용성 좋습니다.


106-117: 모달 z-index를 메뉴보다 높게 설정하세요

파일: apps/client/src/pages/myBookmark/MyBookmark.tsx (lines 106–117)
OptionsMenuPortal(zIndex 10000)보다 모달 래퍼(z-[1000])가 낮아 메뉴가 모달 위로 올라올 수 있으니 z 값을 증가시켜 충돌을 방지하세요.

-        <div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog">
+        <div className="fixed inset-0 z-[11000]" aria-modal="true" role="dialog">

접근성(스크린리더/포커스 트랩) 요구가 있으면 모달 오픈 시 body 스크롤 잠금도 함께 검토하세요.

apps/client/src/pages/remind/Remind.tsx (2)

14-20: 앵커드 메뉴 훅 연동이 일관적이고 재사용성 좋습니다.

Remind/MyBookmark 양쪽 모두 동일 패턴으로 맞춘 점 좋습니다.


66-68: openMenu 인자는 적절 — timeRemaining 포맷 확인 필요

  • openMenu(article.category.categoryId, e.currentTarget)는 의도에 부합합니다. (apps/client/src/pages/remind/Remind.tsx:66-68)
  • timeRemaining={article.remindAt}이 디자인 시스템 Card가 기대하는 형식(예: '2시간 남음' 문자열 vs ISO 8601 Datetime)과 일치하는지 확인하십시오. 레포지토리 검사 스크립트가 실패해 props 정의를 확인하지 못했습니다 — components/Card 또는 components/RemindCard에서 timeRemaining prop 타입을 확인하거나 파일 경로를 제공해 재검증하십시오.

Comment on lines +7 to 12
import { REMIND_MOCK_DATA } from '@pages/remind/constants';
import CardEditModal from '@shared/components/cardEditModal/CardEditModal';
import OptionsMenuPortal from '@shared/components/sidebar/OptionsMenuPortal';
import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu';
import { belowOf } from '@shared/utils/anchorPosition';

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

REMIND_MOCK_DATA 의존 제거 및 북마크 데이터로 이름/타이틀 매핑하세요.

북마크 페이지에서 리마인드 목업을 참조하면 잘못된 텍스트가 노출될 수 있습니다. 실제 북마크 API 응답(articles)의 articleId→표시 문자열(URL/제목) 매핑을 구성해 사용하세요.

적용 예:

-import { useState } from 'react';
+import { useMemo, useState } from 'react';
@@
-import { REMIND_MOCK_DATA } from '@pages/remind/constants';
-  const getBookmarkTitle = (id: number | null) =>
-    id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
+  const getBookmarkTitle = (id: number | null) =>
+    id == null ? '' : (titleById.get(id) ?? '');

추가로 아래 매핑을 파일 내 적절한 위치에 선언하세요(예: 쿼리 훅 아래):

// 추가 코드 (선택한 위치에 삽입)
const titleById = useMemo(() => {
  const m = new Map<number, string>();
  readArticles?.articles?.forEach((a) => m.set(a.articleId, a.url || a.memo || ''));
  unreadArticles?.articles?.forEach((a) => {
    if (!m.has(a.articleId)) m.set(a.articleId, a.url || a.memo || '');
  });
  return m;
}, [readArticles?.articles, unreadArticles?.articles]);
🤖 Prompt for AI Agents
apps/client/src/pages/myBookmark/MyBookmark.tsx lines 7-12: The file imports
REMIND_MOCK_DATA and uses mock reminder labels causing wrong text on the
bookmark page; remove that dependency and instead build a runtime map from the
real bookmark API responses (readArticles and unreadArticles) that maps
articleId → display string (prefer url || memo || title or empty string) using
useMemo, then replace all usages of REMIND_MOCK_DATA lookups with lookups into
this titleById map so displayed names come from actual article data.

Comment on lines +2 to +7
import { Badge, Card } from '@pinback/design-system/ui';
import CardEditModal from '@shared/components/cardEditModal/CardEditModal';
import OptionsMenuPortal from '@shared/components/sidebar/OptionsMenuPortal';
import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu';
import { belowOf } from '@shared/utils/anchorPosition';
import { REMIND_MOCK_DATA } from './constants';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

REMIND_MOCK_DATA 제거 및 카테고리 이름은 실제 응답으로 매핑하세요.

이 파일에서는 메뉴가 카테고리 기준으로 열리므로, 목업의 “아이템 id”로 제목을 찾는 현 구현은 불일치입니다. 응답의 categoryId → categoryName 맵을 구성해 사용하세요.

-import { Badge, Card } from '@pinback/design-system/ui';
+import { Badge, Card } from '@pinback/design-system/ui';
@@
-import { REMIND_MOCK_DATA } from './constants';
-  const getItemTitle = (id: number | null) =>
-    id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
+  const getCategoryName = (id: number | null) =>
+    id == null ? '' : (categoryNameById.get(id) ?? '');

추가로 아래 매핑을 선언하세요:

// 추가 코드 (선택한 위치에 삽입)
import { useMemo } from 'react';

const categoryNameById = useMemo(() => {
  const m = new Map<number, string>();
  data?.articles?.forEach((a) => m.set(a.category.categoryId, a.category.categoryName));
  return m;
}, [data?.articles]);

그리고 Portal prop 이름과 호출부를 맞춰 주세요:

-          getCategoryName={getItemTitle}
+          getCategoryName={getCategoryName}
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 2 to 7, remove the
REMIND_MOCK_DATA import and stop using mock item ids to derive menu titles;
instead import useMemo and build a Map of categoryId → categoryName from the
real response (iterate data?.articles and map each a.category.categoryId to
a.category.categoryName) and use that map to look up titles when opening the
category-based menu; add the useMemo declaration as shown in the review and
depend on [data?.articles]; finally, align the Portal prop name and all its call
sites so the OptionsMenuPortal prop and its invocation match (rename either prop
or callers consistently).

Comment on lines +90 to +100
<div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-[2px]"
onClick={() => setIsEditOpen(false)}
/>
<div className="absolute inset-0 flex items-center justify-center p-4">
<CardEditModal onClose={() => setIsEditOpen(false)} />
</div>
</div>
)}
</div>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

모달 z-index 상향

Portal이 zIndex 10000이므로 모달 래퍼를 그보다 높게 잡아 두는 편이 안전합니다.

-        <div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog">
+        <div className="fixed inset-0 z-[11000]" aria-modal="true" role="dialog">
📝 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
{isEditOpen && (
<div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-[2px]"
onClick={() => setIsEditOpen(false)}
/>
<div className="absolute inset-0 flex items-center justify-center p-4">
<CardEditModal onClose={() => setIsEditOpen(false)} />
</div>
</div>
)}
{isEditOpen && (
<div className="fixed inset-0 z-[11000]" aria-modal="true" role="dialog">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-[2px]"
onClick={() => setIsEditOpen(false)}
/>
<div className="absolute inset-0 flex items-center justify-center p-4">
<CardEditModal onClose={() => setIsEditOpen(false)} />
</div>
</div>
)}
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 90 to 100, the modal
wrapper uses z-[1000] which is lower than the portal z-index (10000); update the
wrapper's z-index to be higher than 10000 (e.g., z-[10001] or a suitably higher
Tailwind z class) so the modal appears above the portal, ensuring the backdrop
and modal content render on top; adjust the className on the outer fixed div
accordingly and keep the rest of the structure intact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 카드 수정/삭제 연결

2 participants