Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions packages/image/global.d.ts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이 file이 혹시 필요한가요...?
필요한 지 검토 부탁드립니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.module.css';
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.

@minjeeki

아마 이거 사용하실일 없을거예요. 이거 Template으로 만드신 패키지같은데, vanilla extract 마이그레이션 이전의 상태인가보네요.

이건 module.css 사용할때 있었던 잔재같아요 ~

57 changes: 57 additions & 0 deletions packages/image/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@sipe-team/image",
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.

@minjeeki

side 패키지에도 추가가 되어야할거같아요.

"description": "Image for Sipe Design System",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/sipe-team/side"
},
"type": "module",
"exports": "./src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"lint": "pnpm exec biome lint",
"test": "vitest",
"typecheck": "tsc",
"prepack": "pnpm run build"
},
"devDependencies": {
"@testing-library/jest-dom": "catalog:",
"@testing-library/react": "catalog:",
"@types/react": "catalog:react",
"happy-dom": "catalog:",
"react": "catalog:react",
"react-dom": "catalog:react",
"tsup": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"react": ">= 18",
"react-dom": ">= 18"
},
"publishConfig": {
"access": "public",
"registry": "https://npm.pkg.github.com",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./styles.css": "./dist/index.css"
}
},
"sideEffects": [
"**/*.css"
]
Comment on lines +56 to +61
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.

지금 따로 cssvanilla-extract가 없어서 여기는 불필요할꺼 같습니다 : )

css none으로 가신다면 수정 필요한 부분입니당

}
176 changes: 176 additions & 0 deletions packages/image/src/Image.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Image } from './Image';

const meta: Meta<typeof Image> = {
title: 'Components/Image',
component: Image,
parameters: {
backgrounds: {
default: 'light',
},
},
args: {
src: 'https://picsum.photos/400/300',
alt: '예시 이미지',
width: 400,
height: 300,
},
};

export default meta;
type Story = StoryObj<typeof Image>;

export const Basic: Story = {
parameters: {
docs: {
description: {
story: '기본 이미지 렌더링입니다. 기본 크기와 기본 fit 동작을 확인할 수 있습니다.',
},
},
},
};

export const Fallbacks: Story = {
parameters: {
docs: {
description: {
story: 'fallbackSrc가 있을 때와 없을 때 동작을 한 번에 비교합니다.',
},
},
},
render: (args) => (
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Image
{...args}
alt="fallback with src"
src="https://invalid-url.com/broken.jpg"
fallbackSrc="https://dummyimage.com/400x300/e5e7eb/111827&text=FALLBACK"
/>
<span style={{ fontSize: 12, color: '#71717a' }}>with fallbackSrc</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div
style={{
width: 400,
height: 300,
border: '1px dashed #d4d4d8',
borderRadius: 8,
background: 'repeating-linear-gradient(45deg, #fafafa, #fafafa 10px, #f4f4f5 10px, #f4f4f5 20px)',
}}
>
<Image {...args} alt="error without fallback" src="https://invalid-url.com/broken.jpg" />
</div>
<span style={{ fontSize: 12, color: '#71717a' }}>without fallbackSrc</span>
</div>
</div>
),
args: {
width: 400,
height: 300,
},
};

export const WithPlaceholder: Story = {
parameters: {
docs: {
description: {
story: '로딩 중 placeholder 확인은 DevTools에서 네트워크를 Slow 3G로 설정 후 새로고침하세요.',
},
},
},
args: {
src: 'https://picsum.photos/400/300',
alt: 'placeholder 예시',
placeholder: (
<div
style={{
width: 400,
height: 300,
background: '#e0e0e0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
로딩 중...
</div>
),
},
};

export const WithFill: Story = {
parameters: {
docs: {
description: {
story: 'fill 사용 예시입니다. 부모 컨테이너에 `position: relative`, `width: 400`, `height: 300`이 필요합니다.',
},
},
},
render: (args) => (
<div style={{ position: 'relative', width: 400, height: 300 }}>
<Image {...args} fill />
</div>
),
args: {
src: 'https://picsum.photos/400/300',
},
};

export const Fits: Story = {
parameters: {
docs: {
description: {
story: 'fit 옵션(`contain`, `cover`)을 같은 크기 박스에서 비교하는 예시입니다.',
},
},
},
render: (args) => (
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Image {...args} fit="contain" alt="fit contain" />
<span style={{ fontSize: 12, color: '#71717a' }}>fit: contain</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Image {...args} fit="cover" alt="fit cover" />
<span style={{ fontSize: 12, color: '#71717a' }}>fit: cover</span>
</div>
</div>
),
args: {
src: 'https://picsum.photos/600/200',
width: 300,
height: 300,
},
};

export const ResponsiveWidth: Story = {
parameters: {
docs: {
description: {
story: 'width를 string(100%)으로 전달했을 때 부모 너비를 따라 반응형으로 동작하는 예시입니다.',
},
},
},
render: (args) => (
<div
style={{
width: '100%',
maxWidth: 500,
minWidth: 220,
resize: 'horizontal',
overflow: 'auto',
border: '1px solid #e4e4e7',
padding: 8,
}}
>
<Image {...args} />
</div>
),
args: {
src: 'https://picsum.photos/400/300',
width: '100%',
height: 300,
},
};
99 changes: 99 additions & 0 deletions packages/image/src/Image.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { Image } from './Image';

describe('Image', () => {
it('renders with required src and alt props', () => {
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.

테스트가 꼼꼼하네요 👍🏻

render(<Image src="https://picsum.photos/400/300" alt="sample image" />);

const img = screen.getByRole('img', { name: 'sample image' });
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', 'https://picsum.photos/400/300');
});

it('applies width and height styles for number and string values', () => {
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.

@minjeeki

이 컴포넌트를 next/image 대체로 쓰는 그림은 아니라기보다, native <img />를 감싼 디자인 시스템용 wrapper로 이해했습니다.

그래서 컴포넌트 자체보다 useImageStatus를 범용적으로 재사용할 수 있게 열어두는 쪽이 더 중요해 보여요. next/image 같은 다른 이미지 렌더러에서도 같은 loading/fallback DX를 만들 수 있도록 하고, 그 사용 시나리오를 테스트로 함께 보강하면 좋겠습니다.

render(<Image src="https://picsum.photos/400/300" alt="size test" width={320} height="50%" />);

const img = screen.getByRole('img', { name: 'size test' });
expect(img).toHaveStyle({
width: '320px',
height: '50%',
});
});

it('switches to fallbackSrc once when image load fails', () => {
render(
<Image
src="https://invalid-url.com/broken.jpg"
fallbackSrc="https://dummyimage.com/400x300/e5e7eb/111827&text=FALLBACK"
alt="fallback test"
/>,
);

const img = screen.getByRole('img', { name: 'fallback test' });
fireEvent.error(img);

expect(img).toHaveAttribute('src', 'https://dummyimage.com/400x300/e5e7eb/111827&text=FALLBACK');
});

it('moves to error state and hides image when fallback is unavailable', () => {
render(<Image src="https://invalid-url.com/broken.jpg" alt="error test" width={400} height={300} />);

const img = screen.getByRole('img', { name: 'error test' });
fireEvent.error(img);

expect(img).toHaveStyle({ visibility: 'hidden' });
});

it('shows placeholder while loading and hides it after load', () => {
render(
<Image
src="https://picsum.photos/400/300"
alt="placeholder test"
placeholder={
<div data-testid="placeholder" style={{ width: 400, height: 300 }}>
loading...
</div>
}
/>,
);

const img = screen.getByAltText('placeholder test');
expect(screen.getByTestId('placeholder')).toBeInTheDocument();
expect(img).toHaveStyle({ visibility: 'hidden' });

fireEvent.load(img);

expect(screen.queryByTestId('placeholder')).not.toBeInTheDocument();
expect(img.style.visibility).toBe('');
});

it('applies fill styles when fill is true', () => {
render(<Image src="https://picsum.photos/400/300" alt="fill test" fill />);

const img = screen.getByRole('img', { name: 'fill test' });
expect(img).toHaveStyle({
position: 'absolute',
width: '100%',
height: '100%',
});
});

it('uses lazy loading by default', () => {
render(<Image src="https://picsum.photos/400/300" alt="loading attr test" />);

const img = screen.getByRole('img', { name: 'loading attr test' });
expect(img).toHaveAttribute('loading', 'lazy');
});

it('always calls user onError callback', () => {
const onError = vi.fn();
render(<Image src="https://invalid-url.com/broken.jpg" alt="onError test" onError={onError} />);

const img = screen.getByRole('img', { name: 'onError test' });
fireEvent.error(img);

expect(onError).toHaveBeenCalledTimes(1);
});
});
Loading
Loading