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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"react-hook-form": "^7.76.0",
"shadcn": "^4.7.0",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.2.2",
"tw-animate-css": "^1.4.0",
"zod": "^4.4.3",
"zustand": "^5.0.13"
Expand All @@ -59,15 +60,15 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"happy-dom": "^20.9.0",
"husky": "^8.0.0",
"lint-staged": "^16.4.0",
"prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.8.0",
"tailwindcss": "^4.3.0",
"tsx": "^4.22.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
"husky": "^8.0.0",
"lint-staged": "^16.4.0",
"vitest": "^4.1.6"
},
"lint-staged": {
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions src/components/common/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { Button } from './Button'

describe('Button', () => {
it('variant가 disable이면 별도 disabled prop 없이도 비활성화된다', () => {
render(
<Button variant="disable" size="m">
저장하기
</Button>,
)

expect(screen.getByRole<HTMLButtonElement>('button', { name: '저장하기' }).disabled).toBe(true)
})

it.each(['primary', 'strong', 'error', 'outlined'] as const)(
'variant가 %s이면 기본적으로 활성화된다',
(variant) => {
render(
<Button variant={variant} size="m">
저장하기
</Button>,
)

expect(screen.getByRole<HTMLButtonElement>('button', { name: '저장하기' }).disabled).toBe(
false,
)
},
)

it('disabled prop을 명시하면 variant와 무관하게 그 값을 따른다', () => {
render(
<Button variant="primary" size="m" disabled>
저장하기
</Button>,
)

expect(screen.getByRole<HTMLButtonElement>('button', { name: '저장하기' }).disabled).toBe(true)
})

it('클릭 시 onClick이 호출된다', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(
<Button variant="primary" size="m" onClick={onClick}>
저장하기
</Button>,
)

await user.click(screen.getByRole('button', { name: '저장하기' }))

expect(onClick).toHaveBeenCalledTimes(1)
})

it('leftIcon과 rightIcon을 함께 렌더링한다', () => {
render(
<Button
variant="primary"
size="m"
leftIcon={<span data-testid="left-icon" />}
rightIcon={<span data-testid="right-icon" />}
>
저장하기
</Button>,
)

expect(screen.getByTestId('left-icon')).toBeTruthy()
expect(screen.getByTestId('right-icon')).toBeTruthy()
})
})
67 changes: 67 additions & 0 deletions src/components/common/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import type { VariantProps } from 'tailwind-variants'
import { tv } from '@lib/tv'

const buttonVariants = tv({
base: 'flex items-center justify-center gap-1 text-center transition-colors',
variants: {
variant: {
primary: 'bg-blue-5 text-brand-primary',
strong: 'bg-brand-primary text-fill-normal',
error: 'bg-red-10 text-red-70',
disable: 'bg-fill-alternative text-label-light cursor-not-allowed',
outlined: 'bg-fill-normal border-line-normal text-label-netural border',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

text-label-netural 오타 - outlined 버튼 텍스트 색상 미적용.

neutral의 오타로 보이며, 존재하지 않는 Tailwind 클래스라 outlined variant에서 텍스트 색상 토큰이 실제로 적용되지 않습니다. 다른 variant는 text-label-light(L12), ProjectThumbnailCardtext-label-normal 등 정상적인 네이밍을 따릅니다.

🐛 제안 수정
-      outlined: 'bg-fill-normal border-line-normal text-label-netural border',
+      outlined: 'bg-fill-normal border-line-normal text-label-neutral border',
📝 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
outlined: 'bg-fill-normal border-line-normal text-label-netural border',
outlined: 'bg-fill-normal border-line-normal text-label-neutral border',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Button.tsx` at line 13, Fix the outlined Button variant
class name typo in Button.tsx: the `outlined` entry currently uses
`text-label-netural`, which is not a valid Tailwind token, so the text color
does not apply. Update the `variant` styling map in `Button` to use the correct
`text-label-neutral` naming, and keep it consistent with the other existing text
color tokens used in this component and related UI code.

},
// pl-*/pr-*로 좌우를 각각 선언한다: tailwind-merge는 px-*를 pl-*/pr-*와 같은 그룹으로
// 취급하지 않아 compoundVariants의 좌우 오버라이드가 조용히 씹힌다(둘 다 남아 CSS 생성
// 순서에 결과가 좌우됨). 같은 축의 유틸리티끼리만 둬야 확실히 병합된다.
size: {
s: 'text-label-14sb h-9 rounded-md pr-4 pl-4',
m: 'text-label-14sb h-10 rounded-lg pr-5 pl-5',
l: 'text-body-16sb h-11 rounded-lg pr-6 pl-6',
xl: 'text-body-16sb h-12 rounded-lg pr-6 pl-6',
},
},
compoundVariants: [
// size=m의 strong만 Figma에서 8px 16px로, 같은 size의 다른 variant(8px 20px)보다 좁다.
{ variant: 'strong', size: 'm', class: 'pr-4 pl-4' },
// outlined는 사이즈와 무관하게 radius 8px + 아이콘 쪽(왼쪽) 패딩이 좁은 비대칭 패딩을 쓴다.
{ variant: 'outlined', size: ['s', 'm'], class: 'rounded-md py-2 pr-4 pl-3' },
{ variant: 'outlined', size: ['l', 'xl'], class: 'rounded-md py-2 pr-5 pl-[18px]' },
],
})

type ButtonVariants = VariantProps<typeof buttonVariants>
export type ButtonVariant = NonNullable<ButtonVariants['variant']>
export type ButtonSize = NonNullable<ButtonVariants['size']>

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant: ButtonVariant
size: ButtonSize
leftIcon?: ReactNode
rightIcon?: ReactNode
}

export function Button({
variant,
size,
leftIcon,
rightIcon,
className,
disabled,
children,
...props
}: ButtonProps) {
return (
<button
type="button"
disabled={disabled ?? variant === 'disable'}
className={buttonVariants({ variant, size, className })}
{...props}
>
{leftIcon}
{children}
{rightIcon}
</button>
)
}
9 changes: 3 additions & 6 deletions src/components/common/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import { cn } from '@lib/utils'
import { Button } from './Button'

interface PageHeaderProps {
title: string
Expand All @@ -22,13 +23,9 @@ export function PageHeader({ title, actionLabel, onAction, children, className }
{children}
</div>
{actionLabel && (
<button
type="button"
onClick={onAction}
className="bg-brand-primary text-label-14sb text-fill-normal h-10 rounded-[8px] px-4 py-2 transition-colors hover:brightness-95"
>
<Button variant="strong" size="m" onClick={onAction} className="hover:brightness-95">
{actionLabel}
</button>
</Button>
)}
</div>
)
Expand Down
1 change: 1 addition & 0 deletions src/components/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @generated

export * from './Button'
export * from './Logo'
export * from './PageHeader'
export * from './SegmentedControl'
Expand Down
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @generated

export * from './common'
export * from './introductions'
export * from './projects'
35 changes: 35 additions & 0 deletions src/components/introductions/ImageBoxThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import UploadIcon from '@/assets/icons/generated/UploadIcon'
import { cn } from '@lib/utils'

interface ImageBoxThumbnailProps {
imageUrl?: string
alt?: string
uploadLabel?: string
className?: string
}

export function ImageBoxThumbnail({
imageUrl,
alt = '',
uploadLabel = '업로드',
className,
}: ImageBoxThumbnailProps) {
return (
<div
className={cn(
'border-fill-alternative relative h-20 w-[150px] overflow-hidden rounded-md border',
!imageUrl && 'bg-fill-normal flex items-center justify-center',
className,
)}
>
{imageUrl ? (
<img src={imageUrl} alt={alt} className="h-full w-full object-cover" />
) : (
<span className="text-caption-12sb text-label-alternative flex items-center gap-1 pr-1.5">
<UploadIcon className="size-[18px]" aria-hidden="true" />
{uploadLabel}
</span>
)}
</div>
)
}
22 changes: 22 additions & 0 deletions src/components/introductions/Inputfield.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { InputHTMLAttributes } from 'react'
import { cn } from '@lib/utils'

interface InputfieldProps extends InputHTMLAttributes<HTMLInputElement> {
className?: string
}

export function Inputfield({ className, ...props }: InputfieldProps) {
return (
<div
className={cn(
'bg-fill-netural border-line-normal flex h-10 items-center rounded-lg border px-3 py-1.5',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'fill-netural|label-assitive|fill-neutral|label-assistive' --type=ts --type=tsx -g '!**/dist/**'

Repository: kusitms-com/makers-admin-fe

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== matching files =="
git ls-files 'src/components/introductions/*' 'src/**/*.{ts,tsx}' | sed -n '1,120p'

echo
echo "== search typos/usages =="
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' \
  'fill-netural|label-assitive|fill-neutral|label-assistive' src

echo
echo "== inspect relevant files =="
for f in \
  src/components/introductions/Inputfield.tsx \
  src/components/introductions/IntroductionCard.tsx \
  src/components/introductions/PartImageCard.tsx
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,120p'
  fi
done

Repository: kusitms-com/makers-admin-fe

Length of output: 1552


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file names =="
fd -a 'Inputfield.tsx|IntroductionCard.tsx|PartImageCard.tsx' src 2>/dev/null || true

echo
echo "== search exact strings in repo =="
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' \
  'fill-netural|label-assitive|fill-neutral|label-assistive|bg-fill-netural|text-label-assitive' .

echo
echo "== show line numbers around Inputfield.tsx if present =="
f='src/components/introductions/Inputfield.tsx'
if [ -f "$f" ]; then
  nl -ba "$f" | sed -n '1,80p'
fi

Repository: kusitms-com/makers-admin-fe

Length of output: 1413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files of interest =="
git ls-files 'src/components/introductions/Inputfield.tsx' 'src/components/introductions/IntroductionCard.tsx' 'src/components/introductions/PartImageCard.tsx' 'src/**/*tokens*' 'src/**/*token*' 'tailwind.config.*' 'postcss.config.*' 'src/**/*css*' | sed -n '1,200p'

echo
echo "== exact string search =="
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' \
  'fill-netural|label-assitive|fill-neutral|label-assistive|bg-fill-netural|text-label-assitive' src . \
  || true

echo
echo "== inspect the three introduction files =="
for f in \
  src/components/introductions/Inputfield.tsx \
  src/components/introductions/IntroductionCard.tsx \
  src/components/introductions/PartImageCard.tsx
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,120p'
  fi
done

Repository: kusitms-com/makers-admin-fe

Length of output: 2264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tailwind / design token files =="
git ls-files | rg '(^|/)(tailwind\.config\.[jt]s|postcss\.config\.[jt]s|tokens?|theme|globals\.css|index\.css|app\.css|styles\.css|tailwind).*'

echo
echo "== search token definitions / class names =="
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' \
  'fill-netural|label-assitive|fill-neutral|label-assistive|addUtilities|matchUtilities|theme\(|tokens|design tokens|colors:' \
  src . \
  || true

echo
echo "== inspect tailwind config candidates =="
for f in $(git ls-files | rg '(^|/)tailwind\.config\.[jt]s$|(^|/)postcss\.config\.[jt]s$'); do
  echo "--- $f ---"
  sed -n '1,240p' "$f"
done

Repository: kusitms-com/makers-admin-fe

Length of output: 3917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/index.css =="
sed -n '1,240p' src/index.css

echo
echo "== package.json =="
sed -n '1,220p' package.json

echo
echo "== src/lib/utils.ts =="
sed -n '1,120p' src/lib/utils.ts

echo
echo "== search for `@import/`@theme/@source/tokens references =="
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' \
  '`@import`|`@theme`|`@source`|`@utility`|`@custom-variant`|`@plugin`|tokens' src . \
  || true

Repository: kusitms-com/makers-admin-fe

Length of output: 11055


🌐 Web query:

"site:github.com fill-netural label-assitive @kusitms.com/tokens"

💡 Result:

Result summary for query: site:github.com fill-netural label-assitive @kusitms.com/tokens I could not find any GitHub pages that match the terms “fill-netural”, “label-assitive”, and “@kusitms.com/tokens” together. What I did find that is closest: 1) A KUSITMS official website repository exists at https://github.com/kusitms-com/kusitms.com [1], with active development (last push shown as 2026-06-07) [1]. However, the search results surfaced only a page component (e.g., src/app/projects/meetup/[projectNumber]/page.tsx) and did not show any “tokens” file or any “fill-netural/label-assitive” strings in the returned snippets [2]. 2) A “Netural-guidelines-assistant” repo appears to be related to “Netural” (spelling “Netural”, not “netural”), but it is not under kusitms.com and the snippets show validation of design tokens rather than kusitms token files [3]. Conclusion - There is no source-backed evidence (from the search results available) that the exact pattern “fill-netural label-assitive @kusitms.com/tokens” exists anywhere on GitHub. - If you meant a different casing/typo (e.g., “neutral” vs “netural”, “label-assist” vs “label-assitive”, or a specific filename/path under the kusitms-com/kusitms.com repo), tell me the exact intended spelling or the target file path and I can re-run a tighter search.

Citations:


클래스명 오타 수정 bg-fill-netural, text-label-assitivebg-fill-neutral, text-label-assistive 오타로 보입니다. 같은 문자열이 IntroductionCard.tsx, PartImageCard.tsx, Sidebar.tsx, SegmentedControl.tsx에도 반복되니 함께 정리하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/introductions/Inputfield.tsx` at line 12, Fix the Tailwind
class name typos in the shared UI styles: `bg-fill-netural` should be
`bg-fill-neutral`, and `text-label-assitive` should be `text-label-assistive`.
Update the affected string(s) in `Inputfield.tsx` and also search the repeated
occurrences in `IntroductionCard.tsx`, `PartImageCard.tsx`, `Sidebar.tsx`, and
`SegmentedControl.tsx` so the same typo is corrected consistently across those
components.

Source: Path instructions

className,
)}
>
<input
className="text-label-14m text-label-normal placeholder:text-label-assitive w-full bg-transparent outline-none"
{...props}
/>
</div>
)
}
50 changes: 50 additions & 0 deletions src/components/introductions/IntroductionCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { IntroductionCard } from './IntroductionCard'

describe('IntroductionCard', () => {
it('default variant에서는 플레이스홀더를 보여주고 추가하기 버튼이 비활성화된다', () => {
render(<IntroductionCard variant="default" />)

expect(screen.getByPlaceholderText('타이틀')).toBeTruthy()
expect(screen.getByPlaceholderText('설명을 입력해주세요')).toBeTruthy()
expect(screen.getByRole<HTMLButtonElement>('button', { name: '추가하기' }).disabled).toBe(true)
expect(screen.getByRole<HTMLButtonElement>('button', { name: '삭제하기' }).disabled).toBe(false)
})

it('active variant에서는 입력값을 보여주고 교체하기 버튼이 활성화된다', () => {
render(
<IntroductionCard
variant="active"
title="내용"
description="큐시즘 오리엔테이션 소개"
thumbnailUrl="/thumb.png"
/>,
)

expect(screen.getByDisplayValue('내용')).toBeTruthy()
expect(screen.getByDisplayValue('큐시즘 오리엔테이션 소개')).toBeTruthy()
expect(screen.getByRole<HTMLButtonElement>('button', { name: '교체하기' }).disabled).toBe(false)
})

it('삭제하기 버튼 클릭 시 onDelete가 호출된다', async () => {
const user = userEvent.setup()
const onDelete = vi.fn()
render(<IntroductionCard variant="active" onDelete={onDelete} />)

await user.click(screen.getByRole('button', { name: '삭제하기' }))

expect(onDelete).toHaveBeenCalledTimes(1)
})

it('active variant에서 교체하기 버튼 클릭 시 onAddOrReplace가 호출된다', async () => {
const user = userEvent.setup()
const onAddOrReplace = vi.fn()
render(<IntroductionCard variant="active" onAddOrReplace={onAddOrReplace} />)

await user.click(screen.getByRole('button', { name: '교체하기' }))

expect(onAddOrReplace).toHaveBeenCalledTimes(1)
})
})
Loading
Loading