Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/client/src/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Sidebar } from '../shared/components/sidebar/Sidebar';
const Layout = () => {
return (
<>
{/* TODO: 필요시 레이아웃 추가 */}
{/* TODO: 사이드바 추가 */}

<div className="flex h-screen">
<Sidebar />
<Outlet />
<main className="flex-1 overflow-y-auto">
<Outlet />
</main>
</div>
</>
);
};
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/pages/level/Level.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function Level() {
</div>
</div>

<Badge text="오늘 모은 도토리 개수" countNum={acorns} />
<Badge text="오늘 모은 도토리 개수" countNum={acorns} isActive={true} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

불리언 리터럴 제거(축약형 사용) + 활성 상태 의도 확인

JSX에서는 isActivetrue일 때 속성 축약이 가능합니다. 또한 이 배지가 Level 페이지에서 항상 활성 스타일이어야 하는지 의도를 한 번만 확인해주세요(토글/선택 개념이 아니라 상시 표시라면 OK).

다음처럼 간소화 가능:

-            <Badge text="오늘 모은 도토리 개수" countNum={acorns} isActive={true} />
+            <Badge text="오늘 모은 도토리 개수" countNum={acorns} isActive />

리팩터 후 디자인시스템 변경(isActive 필수)에 따라 다른 사용처 누락이 없는지 확인하려면 아래 스크립트를 실행하세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Badge를 import하는 파일 나열
mapfile -t FILES < <(rg -nP --type=ts --type=tsx 'import\s*{\s*Badge\s*}\s*from' -o --no-filename | cut -d: -f1 | sort -u)
if [ "${#FILES[@]}" -eq 0 ]; then
  # fallback: 파일 경로를 함께 뽑기
  mapfile -t FILES < <(rg -nP --type=ts --type=tsx 'import\s*{\s*Badge\s*}\s*from' | cut -d: -f1 | sort -u)
fi

# 2) 각 파일에서 <Badge ...> 사용시 isActive 존재 여부 확인
for f in "${FILES[@]}"; do
  echo "=== $f ==="
  rg -nP -C2 '<Badge\b' "$f" || true
  if ! rg -nP '(<Badge\b[^>]*\bisActive\b)' "$f" >/dev/null; then
    echo "[WARN] isActive prop 누락 가능성: $f"
  fi
done

Length of output: 84


기존 스크립트가 bash의 프로세스 치환과 rg --type=tsx 인식 오류로 실패했습니다. 아래 스크립트로 Badge 사용처를 다시 조회하고 isActive 누락 여부를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Badge를 import하는 파일 나열 (*.ts, *.tsx)
mapfile -t FILES < <(rg -l -g '*.ts' -g '*.tsx' "import\s*{\s*Badge\s*}\s*from")

if [ "${#FILES[@]}" -eq 0 ]; then
  echo "Badge를 import한 파일이 없습니다."
  exit 0
fi

# 2) 각 파일에서 <Badge ...> 사용 시 isActive 존재 여부 확인
for f in "${FILES[@]}"; do
  echo "=== $f ==="
  rg -nC2 '<Badge\b' "$f" || true
  if ! rg -q '<Badge\b[^>]*\bisActive\b' "$f"; then
    echo "[WARN] isActive prop 누락 의심: $f"
  fi
done

Length of output: 178


아래 스크립트는 중괄호를 이스케이프하고 PCRE 옵션(-P)을 사용해 Badge import 파일을 찾은 뒤 isActive 누락 여부를 확인합니다. 실행 후 결과를 공유해주세요.


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Badge를 import하는 파일 검색 (*.ts, *.tsx)
rg -lP -g '*.ts' -g '*.tsx' 'import\s*\{\s*Badge\s*\}\s*from' | while IFS= read -r f; do
  echo "=== $f ==="
  rg -nP -C2 '<Badge\b' "$f" || true
  if ! rg -qP '<Badge\b[^>]*\bisActive\b' "$f"; then
    echo "[WARN] isActive prop 누락 의심: $f"
  fi
done

Length of output: 280


아래 스크립트로 Badge 사용처를 다시 검사하고, isActive 속성 누락 여부를 확인해주세요. 실행 후 결과를 공유해 주세요.


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# 1) Badge를 import하는 파일 검색 (*.ts, *.tsx)
FILES=$(rg -lP -g '*.ts' -g '*.tsx' 'import\s*\{\s*Badge\s*\}\s*from')
if [ -z "$FILES" ]; then
  echo "Badge를 import한 파일이 없습니다."
  exit 0
fi

# 2) 각 파일에서 <Badge ...> 사용 줄별로 확인
for f in $FILES; do
  echo "=== $f ==="
  rg -nP '<Badge\b[^>]*>' "$f" | while IFS= read -r line; do
    echo "$line"
    if [[ "$line" != *isActive* ]]; then
      # line 형식: 파일경로:라인번호:코드
      location="${line%%:*}:${line#*:}"
      echo "[WARN] isActive prop 누락 의심: $location"
    fi
  done
done

Length of output: 60


아래 스크립트로 Badge 사용처 전부를 나열하고 isActive 누락 여부를 동시에 확인해 보세요. 실행 후 결과를 공유해 주세요.


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "▶ Badge 사용처 및 isActive 누락 점검"

# 모든 ts/tsx 파일에서 <Badge 사용 줄 찾기
rg -nP '<Badge\b' -g '*.{ts,tsx}' | while IFS= read -r line; do
  # 파일:라인:코드 분리
  file=${line%%:*}
  rest=${line#*:}
  lineno=${rest%%:*}
  code=${rest#*:*}
  
  # 출력
  echo "$file:$lineno: $code"
  
  # isActive 포함 여부 확인
  if ! echo "$code" | grep -q 'isActive'; then
    echo "[WARN] isActive prop 누락 의심: $file:$lineno"
  fi
done

Length of output: 387


Badge 컴포넌트 isActive prop 일관성 적용

  • apps/client/src/pages/level/Level.tsx (45): isActive={true}isActive로 축약
  • apps/client/src/pages/remind/Remind.tsx (16, 22): isActive prop 누락 → 디자인 시스템 변경(isActive 필수)에 따라 추가 필요
  • Level 페이지 배지가 항상 활성 상태로 유지되는 것이 의도된 동작인지 확인
🤖 Prompt for AI Agents
In apps/client/src/pages/level/Level.tsx around line 45 and
apps/client/src/pages/remind/Remind.tsx around lines 16 and 22, the Badge
component usage must follow the design-system change that makes the isActive
prop required; replace verbose isActive={true} with the shorthand isActive in
Level.tsx (if the badge should always be active) and add isActive (true or false
as appropriate) to the two Badge usages in Remind.tsx; also confirm with product
whether the Level page badge is intended to always be active—if not, set
isActive based on the correct state/logic.

<div className="flex">
<TreeStatusCard acorns={acorns} />
</div>
Expand Down
44 changes: 43 additions & 1 deletion apps/client/src/pages/myBookmark/MyBookmark.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
import { REMIND_MOCK_DATA } from "@pages/remind/constants";
import { Badge, Card } from "@pinback/design-system/ui";
import { useState } from "react";
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

배지 상태가 리스트/카운트에 반영되지 않습니다

배지 클릭 시 state는 바뀌지만 목록은 필터링되지 않고 배지 카운트도 하드코딩되어 있습니다. 최소한 현재 목데이터 기준(예: timeRemaining !== '완료'를 미열람으로 간주)으로 동작하도록 보완해 주세요.

-import { useState } from "react";
+import { useMemo, useState } from "react";
-  const [activeBadge, setActiveBadge] = useState('all');
+  const [activeBadge, setActiveBadge] = useState<'all' | 'notRead'>('all');
-  const handleBadgeClick = (badgeType: string) => {
+  const handleBadgeClick = (badgeType: 'all' | 'notRead') => {
     setActiveBadge(badgeType);
   };
-          countNum={5}
+          countNum={counts.all}
-          countNum={10}
+          countNum={counts.notRead}
-{REMIND_MOCK_DATA.map((data) => (
+{filtered.map((data) => (

아래 코드를 Line 10 바로 아래에 추가해 주세요(새 라인 추가):

// 임시 기준: '완료'가 아닌 항목을 '안 읽음'으로 간주
const filtered = useMemo(() => {
  if (activeBadge === 'notRead') {
    return REMIND_MOCK_DATA.filter((item) => item.timeRemaining !== '완료');
  }
  return REMIND_MOCK_DATA;
}, [activeBadge]);

const counts = useMemo(() => {
  const all = REMIND_MOCK_DATA.length;
  const notRead = REMIND_MOCK_DATA.filter((item) => item.timeRemaining !== '완료').length;
  return { all, notRead };
}, []);

Also applies to: 6-6, 8-10, 18-18, 24-24, 32-32

🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/MyBookmark.tsx around line 3 and insert at
line 10, the badge state changes but the list and counts are not updated; update
the import to include useMemo (import { useState, useMemo } from "react"), then
add two useMemo hooks just below line 10: one that computes filtered list based
on activeBadge (treat items with timeRemaining !== '완료' as "notRead") and one
that computes counts (all and notRead) from REMIND_MOCK_DATA; ensure the
counts/useMemo dependency array references REMIND_MOCK_DATA and the filtered
useMemo depends on activeBadge and REMIND_MOCK_DATA, and replace any hardcoded
badge counts and list source with these new values.


const MyBookmark = () => {
return <div>MyBookmark</div>;
const [activeBadge, setActiveBadge] = useState('all');

const handleBadgeClick = (badgeType: string) => {
setActiveBadge(badgeType);
};

return (
<div className="flex flex-col pl-[8rem] py-[5.2rem]">
<p className="head3">나의 북마크</p>
<div className="mt-[3rem] flex gap-[2.4rem]">
<Badge
text="전체보기"
countNum={5}
onClick={() => handleBadgeClick('all')}
isActive={activeBadge === 'all'}
/>
<Badge
text="안 읽음"
countNum={10}
onClick={() => handleBadgeClick('notRead')}
isActive={activeBadge === 'notRead'}
/>
</div>

<div className="scrollbar-hide mt-[2.6rem] flex flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth max-w-[104rem]">
{/* TODO: API 연결 후 수정 */}
{REMIND_MOCK_DATA.map((data) => (
<Card
key={data.id}
type="bookmark"
title={data.title}
content={data.content}
category={data.category}
date="2024.08.15"
/>
Comment on lines +37 to +40
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

요거 static한 데이터!! 추후에 바꿔주세요

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

넵~ 일단 카드에 쓰인 데이터 모두 mock data여서 api연결 후에 바꾸겠습니다 👍

))}
</div>
</div>
);
};

export default MyBookmark;
44 changes: 43 additions & 1 deletion apps/client/src/pages/remind/Remind.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
import { Badge, Card } from '@pinback/design-system/ui';
import { useState } from 'react';
import { REMIND_MOCK_DATA } from './constants';

const Remind = () => {
return <div>Remind</div>;
const [activeBadge, setActiveBadge] = useState('notRead');

const handleBadgeClick = (badgeType: string) => {
setActiveBadge(badgeType);
};

return (
<div className="flex flex-col pl-[8rem] py-[5.2rem]">
<p className="head3">리마인드</p>
<div className="mt-[3rem] flex gap-[2.4rem]">
<Badge
text="안 읽음"
countNum={5}
onClick={() => handleBadgeClick('notRead')}
isActive={activeBadge === 'notRead'}
/>
<Badge
text="읽음"
countNum={10}
onClick={() => handleBadgeClick('read')}
isActive={activeBadge === 'read'}
/>
</div>

<div className="scrollbar-hide mt-[2.6rem] flex flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth max-w-[104rem]">
{/* TODO: API 연결 후 수정 */}
{REMIND_MOCK_DATA.map((data) => (
<Card
key={data.id}
type="remind"
title={data.title}
content={data.content}
timeRemaining={data.timeRemaining}
category={data.category}
/>
))}
</div>
</div>
);
};

export default Remind;
114 changes: 114 additions & 0 deletions apps/client/src/pages/remind/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
export const REMIND_MOCK_DATA = [
{
id: 1,
title: '리액트 쿼리 강의 듣기',
content: '1강부터 5강까지 완강하고 복습하기',
timeRemaining: '1시간 30분',
category: '공부',
},
{
id: 2,
title: '장보기',
content: '우유, 계란, 채소 구매',
timeRemaining: '3시간 00분',
category: '생활',
},
{
id: 3,
title: '프로젝트 기획서 초안 작성',
content: '주요 기능 및 화면 흐름 정리',
timeRemaining: '완료',
category: '업무',
},
{
id: 4,
title: '헬스장 가기',
content: '하체 운동 루틴 진행',
timeRemaining: '5시간 10분',
category: '운동',
},
{
id: 5,
title: '주간 회고 작성',
content: '이번 주에 배운 점과 개선할 점 정리',
timeRemaining: '완료',
category: '자기계발',
},
{
id: 6,
title: '저녁 약속',
content: '강남역 2번 출구에서 친구 만나기',
timeRemaining: '8시간 00분',
category: '약속',
},
{
id: 7,
title: '알고리즘 문제 풀기',
content: '백준 골드 문제 2개 풀기',
timeRemaining: '2시간 45분',
category: '공부',
},
{
id: 8,
title: '이메일 회신',
content: 'A사에서 온 제휴 문의 메일 확인하고 회신',
timeRemaining: '완료',
category: '업무',
},
{
id: 9,
title: '방 청소하기',
content: '책상 정리 및 바닥 청소기 돌리기',
timeRemaining: '0시간 50분',
category: '생활',
},
{
id: 10,
title: 'TypeScript 스터디 준비',
content: '제네릭 파트 발표 자료 만들기',
timeRemaining: '10시간 00분',
category: '공부',
},
{
id: 11,
title: '은행 업무 보기',
content: '만기 된 예금 재예치하기',
timeRemaining: '완료',
category: '금융',
},
{
id: 12,
title: '블로그 글 작성',
content: '이번 주에 해결한 기술적 문제에 대해 포스팅하기',
timeRemaining: '1일 2시간',
category: '자기계발',
},
{
id: 13,
title: '가족과 통화하기',
content: '부모님께 안부 전화드리기',
timeRemaining: '4시간 20분',
category: '약속',
},
{
id: 14,
title: '주말 산책 계획',
content: '가까운 공원 산책 코스 찾아보기',
timeRemaining: '2일 0시간',
category: '여가',
},
{
id: 15,
title: '팀 회의록 정리',
content: '어제 진행된 스프린트 계획 회의 내용 정리해서 공유',
timeRemaining: '완료',
category: '업무',
},
{
id: 16,
title: '팀 회의록 정리',
content: '어제 진행된 스프린트 계획 회의 내용 정리해서 공유',
timeRemaining: '완료',
category: '업무',
},
];
47 changes: 42 additions & 5 deletions packages/design-system/src/components/badge/Badge.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { within, userEvent } from '@storybook/test';
import { within, userEvent, fn } from '@storybook/test';
import Badge from './Badge';
import { useState } from 'react';

const meta: Meta<typeof Badge> = {
title: 'Components/Badge',
Expand All @@ -9,13 +10,27 @@ const meta: Meta<typeof Badge> = {
args: {
text: '알림',
countNum: 3,
onClick: () => alert('onClick 실행'),
isActive: true,
},
argTypes: {
text: { control: 'text', description: '뱃지 라벨 텍스트' },
countNum: {
control: { type: 'number', min: 0 },
description: '카운트 숫자(옵션)',
},

isActive: {
control: 'boolean',
description: '활성화 여부',
table: {
type: { summary: 'boolean' },
},
},
onClick: {
action: 'clicked',
description: '뱃지 클릭 시 호출되는 콜백 함수(옵션)',
},
},
parameters: {
docs: {
Expand All @@ -33,22 +48,44 @@ type Story = StoryObj<typeof Badge>;
export const Default: Story = {};

export const NoCount: Story = {
args: { text: '카운트 없음', countNum: undefined },
args: { text: '카운트 없음', countNum: 0, isActive: false },
};

export const LargeCount: Story = {
args: { text: '메시지', countNum: 12000 },
};

export const Clicked: Story = {
args: { text: '클릭해줘', countNum: 5 },
export const ActiveBadge: Story = {
render: (args) => {
const ClickedBadge = () => {
const [isActive, setIsActive] = useState(false);

return (
<Badge
{...args}
isActive={isActive}
onClick={() => {
setIsActive((prev) => !prev); // 토글
args.onClick?.(); // 액션 로그
}}
/>
);
};

return <ClickedBadge />;
},
args: {
text: '클릭해줘',
countNum: 5,
onClick: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await userEvent.click(await canvas.findByText(String(args.text)));
},
parameters: {
docs: {
description: { story: '로드 직후 자동 클릭으로 활성 상태 미리보기.' },
description: { story: 'onClick으로 뱃지 활/비활성화' },
},
},
};
27 changes: 17 additions & 10 deletions packages/design-system/src/components/badge/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
import { cva } from 'class-variance-authority';
import { useState } from 'react';
export interface BadgeProps {
text: string;
countNum?: number;
isActive: boolean;
onClick?: () => void;
Comment on lines 4 to +6
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

활성 제어를 위해서 추가가 필요했군녀

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

맞아요! 실제 작동하는 로직은 외부에 책임을 두고 내부에서는 isActive를 통해 활성화가 되었는지만 담당합니다!
이에 따라 스타일만 변경하면 되는 구조여서 이렇게 수정하게 되었습니다.

}

const BadgeTxtStyleVariants = cva('sub3-b', {
variants: {
click: {
active: {
true: 'text-font-black-1',
false: 'text-font-ltgray-4',
} as const,
},
defaultVariants: {
click: false,
active: false,
},
});

const BadgeStyleVariants = cva(
'text-white-bg sub5-sb rounded-[0.4rem] px-[0.8rem] py-[0.4rem]',
{
variants: {
click: {
active: {
true: 'bg-main500',
false: 'bg-gray300',
} as const,
},
defaultVariants: {
click: false,
active: false,
},
}
);
const Badge = ({ text, countNum }: BadgeProps) => {
const [isClick, setIsClick] = useState(false);

const Badge = ({ text, countNum, isActive, onClick }: BadgeProps) => {
return (
<div
className="flex cursor-pointer items-center justify-center gap-[0.8rem]"
onClick={() => setIsClick(true)}
onClick={onClick}
>
<span className={BadgeTxtStyleVariants({ click: isClick })}>{text}</span>
<span className={BadgeStyleVariants({ click: isClick })}>{countNum}</span>
<span className={BadgeTxtStyleVariants({ active: isActive })}>
{text}
</span>
<span className={BadgeStyleVariants({ active: isActive })}>
{countNum}
</span>
Comment on lines +36 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

div+onClick은 키보드 접근 불가 — 네이티브 button으로 교체 필요

현재 구조는 스크린리더/키보드 사용자가 접근하기 어렵습니다. design-system 컴포넌트이므로 초기에 바로잡는 게 좋습니다.

-const Badge = ({ text, countNum, isActive, onClick }: BadgeProps) => {
+const Badge = ({ text, countNum, isActive = false, onClick, ...rest }: BadgeProps) => {
   return (
-    <div
-      className="flex cursor-pointer items-center justify-center gap-[0.8rem]"
-      onClick={onClick}
-    >
+    <button
+      type="button"
+      aria-pressed={isActive}
+      onClick={onClick}
+      className="flex cursor-pointer items-center justify-center gap-[0.8rem] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main500 focus-visible:ring-offset-2"
+      {...rest}
+    >
       <span className={BadgeTxtStyleVariants({ active: isActive })}>
         {text}
       </span>
       <span className={BadgeStyleVariants({ active: isActive })}>
         {countNum}
       </span>
-    </div>
+    </button>
   );
 };
🤖 Prompt for AI Agents
In packages/design-system/src/components/badge/Badge.tsx around lines 36 to 47,
the interactive element is a div with an onClick which is not
keyboard-accessible or semantic; replace the outer div with a native <button>
element, move the className and onClick onto that button, add type="button" to
avoid form submit behavior, preserve styling (including focus/hover states
instead of cursor-pointer), and ensure any needed ARIA attributes/keyboard
handlers are removed because the native button provides accessibility by
default.

</div>
);
};
Expand Down
Loading
Loading