Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
107 changes: 107 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: CI & Release (pnpm, secure + optimized)

on:
push:
branches: [main]
tags:
- 'v*' # v로 시작하는 태그가 푸시될 때 (예: v1.0.0)
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: corepack enable
- name: Cache pnpm store
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install --frozen-lockfile
- run: pnpm test --if-present

release:
needs: test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- run: corepack enable
- name: Cache pnpm store
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install --frozen-lockfile
- run: pnpm build

- name: Configure npm auth
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc

- name: Verify npm user
run: npm whoami
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Pack package for test
run: pnpm pack

- name: Test local install
run: npm install ./$(ls *.tgz | head -n 1)

- name: Publish to npm
run: pnpm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

# 배포 성공 후에만 Release 생성
- name: Create GitHub Release
uses: actions/github-script@v7
with:
script: |
const tagName = context.ref.replace('refs/tags/', '');
try {
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `Release ${tagName}`,
body: `🚀 Package successfully published to NPM!\n\n**Version:** ${tagName}\n**Package:** [recomponent](https://www.npmjs.com/package/recomponent)`,
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

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

The package name in the NPM link should be '@ramong23/recomponent' to match the actual package name defined in package.json.

Suggested change
body: `🚀 Package successfully published to NPM!\n\n**Version:** ${tagName}\n**Package:** [recomponent](https://www.npmjs.com/package/recomponent)`,
body: `🚀 Package successfully published to NPM!\n\n**Version:** ${tagName}\n**Package:** [@ramong23/recomponent](https://www.npmjs.com/package/@ramong23/recomponent)`,

Copilot uses AI. Check for mistakes.
draft: false,
prerelease: tagName.includes('-')
});
console.log(`✅ Release ${tagName} created successfully!`);
} catch (error) {
if (error.status === 422) {
console.log(`⚠️ Release ${tagName} already exists`);
} else {
throw error;
}
}
# 이메일 알림
- name: Send email
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
secure: true
username: ${{ secrets.GMAIL_USERNAME }}
password: ${{ secrets.GMAIL_APP_PASSWORD }}
subject: 🚀 Release ${{ github.ref_name }} completed
body: |
Your package has been successfully published to NPM!
Version: ${{ github.ref_name }}
Package: recomponent
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

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

The package name should be '@ramong23/recomponent' to match the actual package name defined in package.json.

Suggested change
Package: recomponent
Package: @ramong23/recomponent

Copilot uses AI. Check for mistakes.
Repository: ${{ github.repository }}
to: [email protected]
from: [email protected]
35 changes: 33 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
{
"name": "xp-components",
"name": "@ramong23/recomponent",
"private": false,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./styles": "./dist/index.css"
},
"files": [
"dist",
"README.md"
],
"keywords": [
"react",
"components",
"ui",
"typescript"
],
"repository": {
"type": "git",
"url": "https://github.com/ramong26/xp-components.git"
},
"homepage": "https://github.com/ramong26/xp-components",
"bugs": {
"url": "https://github.com/ramong26/xp-components/issues"
},
Comment on lines +27 to +34

Choose a reason for hiding this comment

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

high

repository, homepage, bugs URL에 오타가 있는 것 같습니다. 사용자 이름이 ramong26으로 되어 있는데, author 필드와 패키지 이름을 보면 ramong23이 맞는 것 같습니다. 일관성을 위해 수정하는 것을 권장합니다.

  "repository": {
    "type": "git",
    "url": "https://github.com/ramong23/xp-components.git"
  },
  "homepage": "https://github.com/ramong23/xp-components",
  "bugs": {
    "url": "https://github.com/ramong23/xp-components/issues"
  }

"author": "ramong23",
"license": "MIT",
"scripts": {
"dev": "vite",
"build:lib": "tsup src/index.ts --dts --format esm,cjs --out-dir dist --clean",
Expand Down
Binary file added ramong23-recomponent-0.0.0.tgz
Binary file not shown.
37 changes: 26 additions & 11 deletions src/components/Carousel/Carousel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { test, expect, vi } from 'vitest';

Expand Down Expand Up @@ -30,34 +30,49 @@ const items = [

test('renders Carousel and 클릭', async () => {
render(<Carousel items={items} autoPlay={false} />);
const slide1 = screen.getByRole('heading', { name: /Slide 1/i });
const slide2 = screen.getByRole('heading', { name: /Slide 2/i });
const slide3 = screen.getByRole('heading', { name: /Slide 3/i });
await userEvent.click(slide1);
const indicators = screen.getAllByRole('button');

expect(screen.getByRole('heading', { name: /Slide 1/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('heading', { name: /Slide 1/i }));
expect(items[0].onClick).toHaveBeenCalled();
await userEvent.click(slide2);

await userEvent.click(indicators[1]);
expect(screen.getByRole('heading', { name: /Slide 2/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('heading', { name: /Slide 2/i }));
expect(items[1].onClick).toHaveBeenCalled();
await userEvent.click(slide3);

await userEvent.click(indicators[2]);
expect(screen.getByRole('heading', { name: /Slide 3/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('heading', { name: /Slide 3/i }));
expect(items[2].onClick).toHaveBeenCalled();

Choose a reason for hiding this comment

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

medium

이 테스트 케이스의 로직이 여러 번 반복되고 있습니다. 각 슬라이드에 대한 확인 및 클릭 로직을 루프로 만들어 코드를 더 간결하고 유지보수하기 쉽게 만들 수 있습니다. 이렇게 하면 캐러셀 아이템이 추가되거나 변경될 때 테스트 코드를 더 쉽게 확장할 수 있습니다.

Suggested change
expect(screen.getByRole('heading', { name: /Slide 1/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('heading', { name: /Slide 1/i }));
expect(items[0].onClick).toHaveBeenCalled();
await userEvent.click(slide2);
await userEvent.click(indicators[1]);
expect(screen.getByRole('heading', { name: /Slide 2/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('heading', { name: /Slide 2/i }));
expect(items[1].onClick).toHaveBeenCalled();
await userEvent.click(slide3);
await userEvent.click(indicators[2]);
expect(screen.getByRole('heading', { name: /Slide 3/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('heading', { name: /Slide 3/i }));
expect(items[2].onClick).toHaveBeenCalled();
for (const [index, item] of items.entries()) {
if (index > 0) {
await userEvent.click(indicators[index]);
}
const slide = screen.getByRole('heading', { name: new RegExp(item.title, 'i') });
expect(slide).toBeInTheDocument();
await userEvent.click(slide);
expect(item.onClick).toHaveBeenCalled();
}

});

test('자동 슬라이드 동작 (타이머 기반)', () => {
test('자동 슬라이드 동작 (타이머 기반)', async () => {
vi.useFakeTimers();
render(<Carousel items={items} autoPlay={true} interval={1000} />);

const indicators = screen.getAllByRole('button');

expect(indicators[0]).toHaveClass('active');

vi.advanceTimersByTime(1000);
await act(async () => {
vi.advanceTimersByTime(1000);
});

expect(indicators[0]).not.toHaveClass('active');
expect(indicators[1]).toHaveClass('active');

vi.advanceTimersByTime(1000);
await act(async () => {
vi.advanceTimersByTime(1000);
});

expect(indicators[1]).not.toHaveClass('active');
expect(indicators[2]).toHaveClass('active');

vi.advanceTimersByTime(1000);
await act(async () => {
vi.advanceTimersByTime(1000);
});

expect(indicators[2]).not.toHaveClass('active');
expect(indicators[0]).toHaveClass('active');

Choose a reason for hiding this comment

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

medium

자동 슬라이드 동작을 검증하는 테스트 코드에 반복되는 로직이 있습니다. actexpect 호출이 여러 번 반복되는데, 이를 루프로 리팩토링하면 코드가 더 간결해지고 유지보수하기 쉬워집니다. 예를 들어, 캐러셀 아이템 개수가 변경되어도 테스트 코드를 수정할 필요가 없어집니다.

Suggested change
await act(async () => {
vi.advanceTimersByTime(1000);
});
expect(indicators[0]).not.toHaveClass('active');
expect(indicators[1]).toHaveClass('active');
vi.advanceTimersByTime(1000);
await act(async () => {
vi.advanceTimersByTime(1000);
});
expect(indicators[1]).not.toHaveClass('active');
expect(indicators[2]).toHaveClass('active');
vi.advanceTimersByTime(1000);
await act(async () => {
vi.advanceTimersByTime(1000);
});
expect(indicators[2]).not.toHaveClass('active');
expect(indicators[0]).toHaveClass('active');
for (let i = 0; i < items.length; i++) {
const currentIndex = i;
const nextIndex = (i + 1) % items.length;
await act(async () => {
vi.advanceTimersByTime(1000);
});
expect(indicators[currentIndex]).not.toHaveClass('active');
expect(indicators[nextIndex]).toHaveClass('active');
}


Expand Down
Loading