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
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

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:** [@ramong26/xp-components](https://www.npmjs.com/package/@ramong26/xp-components)`,
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": "@ramong26/xp-components",
"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.
46 changes: 24 additions & 22 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,36 +30,38 @@ 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);
expect(items[0].onClick).toHaveBeenCalled();
await userEvent.click(slide2);
expect(items[1].onClick).toHaveBeenCalled();
await userEvent.click(slide3);
expect(items[2].onClick).toHaveBeenCalled();
const indicators = screen.getAllByRole('button');

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);
expect(indicators[0]).not.toHaveClass('active');
expect(indicators[1]).toHaveClass('active');

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

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');
}

vi.useRealTimers();
});