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
3 changes: 2 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ const config: Config = {
],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.svg$': '<rootDir>/src/__mocks__/fileMock.js', // SVG 파일을 모킹
'^@/(.*)$': '<rootDir>/src/$1', // 다른 경로 매핑
},
}

Expand Down
1 change: 1 addition & 0 deletions src/__mocks__/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'test-file-stub'
2 changes: 1 addition & 1 deletion src/assets/icons/ic-close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions src/components/common/chip/Chip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'

import { Chip } from './Chip'

describe('Chip Component', () => {
test('renders the Chip component with correct label', () => {
// Chip 컴포넌트가 올바른 라벨을 렌더링하는지 확인합니다.
render(<Chip label='모집 중' />)
const chipElement = screen.getByText(/모집 중/i)
expect(chipElement).toBeInTheDocument()
})

test('applies correct styles for each label', () => {
// 각 라벨에 대해 올바른 스타일이 적용되는지 확인합니다.
const labelStyles = {
'모집 중': 'bg-blue-100 text-blue-500',
'모집 완료': 'bg-gray-200 text-gray-600',
스터디: 'bg-green-100 text-green-500',
프로젝트: 'bg-purple-100 text-purple-500',
멘토링: 'bg-red-100 text-red-500',
기술: 'bg-blue-100 text-blue-500',
커리어: 'bg-pink-100 text-pink-500',
기타: 'bg-orange-100 text-orange-500',
}

Object.entries(labelStyles).forEach(([label, expectedClasses]) => {
render(<Chip label={label} />)
const chipElement = screen.getByText(label)

// expectedClasses에 포함된 각 클래스가 chipElement에 포함되는지 확인합니다.
expectedClasses.split(' ').forEach(className => {
expect(chipElement).toHaveClass(className)
})
})
})

test('applies additional className passed as prop', () => {
// className prop을 통해 추가된 클래스가 Chip 컴포넌트에 적용되는지 확인합니다.
render(<Chip label='기술' className='custom-class' />)
const chipElement = screen.getByText(/기술/i)
expect(chipElement).toHaveClass('custom-class')
})

test('applies base styles correctly', () => {
// 기본 스타일이 Chip 컴포넌트에 올바르게 적용되는지 확인합니다.
render(<Chip label='커리어' />)
const chipElement = screen.getByText(/커리어/i)
expect(chipElement).toHaveClass(
'flex',
'h-28',
'items-center',
'justify-center',
'rounded-4',
'px-6',
'text-body3',
'font-medium'
)
})

test('applies default styles when label is not in styleByLabel', () => {
// styleByLabel에 없는 라벨에 대해 기본 스타일이 적용되는지 확인합니다.
render(<Chip label='기타 라벨' />)
const chipElement = screen.getByText(/기타 라벨/i)
expect(chipElement).toHaveClass('bg-gray-200', 'text-gray-500')
})
})
26 changes: 26 additions & 0 deletions src/components/common/chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import clsx from 'clsx'

type ChipProps = {
label: string
className?: string
}

const baseStyle =
'flex h-28 items-center justify-center rounded-4 bg-gray-200 px-6 text-body3 font-medium text-gray-500'

const styleByLabel: Record<string, string> = {
'모집 중': 'bg-blue-100 text-blue-500',
'모집 완료': 'bg-gray-200 text-gray-600',
스터디: 'bg-green-100 text-green-500',
프로젝트: 'bg-purple-100 text-purple-500',
멘토링: 'bg-red-100 text-red-500',
기술: 'bg-blue-100 text-blue-500',
커리어: 'bg-pink-100 text-pink-500',
기타: 'bg-orange-100 text-orange-500',
}

export const Chip = ({ label, className = '' }: ChipProps): JSX.Element => {
const labelStyle = styleByLabel[label] || ''
const chipStyle = clsx(baseStyle, labelStyle, className)
return <span className={chipStyle}>{label}</span>
}
62 changes: 62 additions & 0 deletions src/components/common/chip/DeletableChip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import '@testing-library/jest-dom'
import { fireEvent, render, screen } from '@testing-library/react'

import { DeletableChip } from './DeletableChip'

describe('DeletableChip Component', () => {
test('renders the chip with correct label', () => {
// 올바른 label이 렌더링되는지 확인합니다.
render(<DeletableChip label='스터디' onDelete={() => {}} />)
const chipElement = screen.getByText(/스터디/i)
expect(chipElement).toBeInTheDocument()
})

test('renders with the default blue color style', () => {
// 기본 색상이 blue로 렌더링되는지 확인합니다.
render(<DeletableChip label='스터디' onDelete={() => {}} />)
const chipElement = screen.getByText(/스터디/i)
expect(chipElement).toHaveClass('bg-blue-800', 'text-common-white')
})

test('renders with the gray color style when specified', () => {
// gray 색상이 지정되었을 때 올바르게 스타일링되는지 확인합니다.
render(<DeletableChip label='스터디' color='gray' onDelete={() => {}} />)
const chipElement = screen.getByText(/스터디/i)
expect(chipElement).toHaveClass(
'border-1',
'border-solid',
'border-gray-200',
'bg-gray-100',
'text-gray-700'
)
})

test('renders delete button with gray color style when chip color is gray', () => {
// chip이 gray일 때 delete 버튼이 gray 스타일을 가지는지 확인합니다.
render(<DeletableChip label='스터디' color='gray' onDelete={() => {}} />)
const buttonElement = screen.getByRole('button', { name: /스터디 삭제/i })
expect(buttonElement).toHaveClass('text-gray-400')
})

test('calls onDelete when delete button is clicked', () => {
// 삭제 버튼 클릭 시 onDelete가 호출되는지 확인합니다.
const handleDelete = jest.fn()
render(<DeletableChip label='스터디' onDelete={handleDelete} />)
const buttonElement = screen.getByRole('button', { name: /스터디 삭제/i })
fireEvent.click(buttonElement)
expect(handleDelete).toHaveBeenCalledTimes(1)
})

test('applies additional className when specified', () => {
// 추가된 className이 적용되는지 확인합니다.
render(
<DeletableChip
label='스터디'
onDelete={() => {}}
className='custom-class'
/>
)
const chipElement = screen.getByText(/스터디/i)
expect(chipElement).toHaveClass('custom-class')
})
})
42 changes: 42 additions & 0 deletions src/components/common/chip/DeletableChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { IcClose } from '@/assets/IconList'
import clsx from 'clsx'

type Color = 'blue' | 'gray'

type ChipProps = {
onDelete: () => void
color?: Color
label: string
className?: string
}

const baseStyle =
'flex h-36 items-center justify-center gap-4 rounded-4 px-8 text-body2 font-medium'

const styleByColor = {
blue: 'bg-blue-800 text-common-white',
gray: 'border-1 border-solid border-gray-200 bg-gray-100 text-gray-700',
}

export const DeletableChip = ({
onDelete,
label,
color = 'blue',
className = '',
}: ChipProps): JSX.Element => {
const chipStyle = clsx(baseStyle, styleByColor[color], className)
const buttonStyle = clsx({ 'text-gray-400': color === 'gray' })
return (
<span className={chipStyle}>
{label}
<button
onClick={onDelete}
className={buttonStyle}
type='button'
aria-label={`${label} 삭제`}
>
<IcClose />
</button>
</span>
)
}
4 changes: 4 additions & 0 deletions src/components/common/chip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Chip } from './Chip'
import { DeletableChip } from './DeletableChip'

export { Chip, DeletableChip }
39 changes: 39 additions & 0 deletions src/stories/common/chip/Chip.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { fn } from '@storybook/test'

import { Chip } from '@/components/common/chip'

export default {
title: 'Common/Chip/Chip',
component: Chip,
parameters: {
layout: 'centered',
},
tags: ['common', 'chip'],
argTypes: {
label: {
control: 'radio',
options: [
'모집 중',
'모집 완료',
'스터디',
'프로젝트',
'멘토링',
'기술',
'커리어',
'기타',
'프론트엔드',
'#Spring',
],
description: '문자열만 받습니다.',
},
},
args: {
onClick: fn(),
},
}

export const Default = {
args: {
label: '스터디',
},
}
43 changes: 43 additions & 0 deletions src/stories/common/chip/DeletableChip.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { fn } from '@storybook/test'

import { DeletableChip } from '@/components/common/chip'

export default {
title: 'Common/Chip/DeletableChip',
component: DeletableChip,
parameters: {
layout: 'centered',
},
tags: ['common', 'chip', 'deletable'],
argTypes: {
label: {
control: 'radio',
options: [
'Spring',
'ReactNative',
'MongoDB',
'TypeScript',
'프론트엔드',
'풀스택',
'DevOps 엔지니어',
],
description: '문자열만 받습니다.',
},
},
args: {
onDelete: fn(),
},
}

export const Default = {
args: {
label: '스터디',
},
}

export const Gray = {
args: {
label: '기술',
color: 'gray',
},
}
1 change: 1 addition & 0 deletions src/types/community.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type CommunityCategory = 'SKILL' | 'CAREER' | 'OTHER'
export type CommunityLabelCategory = '기술' | '커리어' | '기타'

export interface CommunityCreateRequest {
communityCategory?: CommunityCategory
Expand Down
4 changes: 4 additions & 0 deletions src/types/svg.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.svg' {
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>
export default ReactComponent
}
2 changes: 2 additions & 0 deletions src/types/team.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export type TeamType = 'STUDY' | 'PROJECT' | 'MENTORING'
export type TeamLabelType = '스터디' | '프로젝트' | '멘토링'
export type TeamRecruitmentLabelType = '모집 중' | '모집 완료'
export type TeamPosition = string

export interface TeamCreateRequest {
Expand Down
40 changes: 14 additions & 26 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,33 +82,22 @@ const colorPalette: Record<

const fontPalette: Record<
string,
{
size: string
lineHeight: string
}
[string, { lineHeight: string; letterSpacing: string }]
> = {
heading1: { size: '40px', lineHeight: '54px' },
heading2: { size: '28px', lineHeight: '36px' },
heading3: { size: '24px', lineHeight: '32px' },
heading4: { size: '22px', lineHeight: '30px' },
heading5: { size: '20px', lineHeight: '28px' },
title1: { size: '18px', lineHeight: '26px' },
title2: { size: '16px', lineHeight: '24px' },
body1: { size: '16px', lineHeight: '24px' },
body2: { size: '15px', lineHeight: '22px' },
body3: { size: '14px', lineHeight: '20px' },
caption1: { size: '13px', lineHeight: '18px' },
caption2: { size: '12px', lineHeight: '16px' },
heading1: ['40px', { lineHeight: '54px', letterSpacing: '-0.02em' }],
heading2: ['28px', { lineHeight: '36px', letterSpacing: '-0.02em' }],
heading3: ['24px', { lineHeight: '32px', letterSpacing: '-0.02em' }],
heading4: ['22px', { lineHeight: '30px', letterSpacing: '-0.02em' }],
heading5: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }],
title1: ['18px', { lineHeight: '26px', letterSpacing: '-0.02em' }],
title2: ['16px', { lineHeight: '24px', letterSpacing: '-0.02em' }],
body1: ['16px', { lineHeight: '24px', letterSpacing: '-0.02em' }],
body2: ['15px', { lineHeight: '22px', letterSpacing: '-0.02em' }],
body3: ['14px', { lineHeight: '20px', letterSpacing: '-0.02em' }],
caption1: ['13px', { lineHeight: '18px', letterSpacing: '-0.02em' }],
caption2: ['12px', { lineHeight: '16px', letterSpacing: '-0.02em' }],
Comment on lines +87 to +98
Copy link
Contributor

Choose a reason for hiding this comment

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

아앗..!!!

}

const fontSize = Object.fromEntries(
Object.entries(fontPalette).map(([key, { size }]) => [key, size])
)

const lineHeight = Object.fromEntries(
Object.entries(fontPalette).map(([key, { lineHeight }]) => [key, lineHeight])
)

const px0_20 = Array.from(Array(21)).reduce(
(acc, _, i) => {
acc[i] = `${i}px`
Expand Down Expand Up @@ -148,8 +137,7 @@ const config: Config = {
lg: { min: '1200px' },
},
borderWidth: px0_20,
fontSize,
lineHeight,
fontSize: fontPalette,
minWidth: px0_1200,
minHeight: px0_1200,
spacing: px0_1200,
Expand Down