feat: Add CI/CD pipeline and code quality improvements
- Add GitHub Actions workflow for automated CI/CD - Configure Node.js 18.x and 20.x matrix testing - Add TypeScript type checking step - Add ESLint code quality checks with enhanced rules - Add Prettier formatting verification - Add production build validation - Upload build artifacts for deployment - Set up automated testing on push/PR - Replace console.log with environment-aware logger - Add pre-commit hooks for code quality - Exclude archive folder from linting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,2 @@
|
||||
|
||||
export const TOAST_LIMIT = 5 // 최대 5개로 제한
|
||||
export const TOAST_REMOVE_DELAY = 3000 // 3초 후 DOM에서 제거
|
||||
export const TOAST_LIMIT = 5; // 최대 5개로 제한
|
||||
export const TOAST_REMOVE_DELAY = 3000; // 3초 후 DOM에서 제거
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { Toast, ToasterToast, State } from "./types";
|
||||
import { actionTypes } from "./types";
|
||||
import { listeners, memoryState } from "./store";
|
||||
import { genId, dispatch } from "./toastManager";
|
||||
|
||||
import * as React from "react"
|
||||
import { Toast, ToasterToast, State } from "./types"
|
||||
import { actionTypes } from "./types"
|
||||
import { listeners, memoryState } from "./store"
|
||||
import { genId, dispatch } from "./toastManager"
|
||||
|
||||
export { TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./constants"
|
||||
export type { ToasterToast } from "./types"
|
||||
export { TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./constants";
|
||||
export type { ToasterToast } from "./types";
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: actionTypes.UPDATE_TOAST,
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
|
||||
});
|
||||
const dismiss = () =>
|
||||
dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: actionTypes.ADD_TOAST,
|
||||
@@ -25,37 +25,40 @@ function toast({ ...props }: Toast) {
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
if (!open) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
duration: props.duration || 3000, // 기본 지속 시간 3초로 설정
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
|
||||
}
|
||||
dismiss: (toastId?: string) =>
|
||||
dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export { useToast, toast };
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
import { TOAST_REMOVE_DELAY, TOAST_LIMIT } from './constants'
|
||||
import { Action, State, actionTypes } from './types'
|
||||
import { dispatch } from './toastManager'
|
||||
import { TOAST_REMOVE_DELAY, TOAST_LIMIT } from "./constants";
|
||||
import { Action, State, actionTypes } from "./types";
|
||||
import { dispatch } from "./toastManager";
|
||||
|
||||
// 토스트 타임아웃 맵
|
||||
export const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
export const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
// 토스트 자동 제거 함수
|
||||
export const addToRemoveQueue = (toastId: string) => {
|
||||
@@ -15,15 +14,15 @@ export const addToRemoveQueue = (toastId: string) => {
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: actionTypes.REMOVE_TOAST,
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
@@ -35,7 +34,7 @@ export const reducer = (state: State, action: Action): State => {
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
};
|
||||
|
||||
case actionTypes.UPDATE_TOAST:
|
||||
return {
|
||||
@@ -43,17 +42,17 @@ export const reducer = (state: State, action: Action): State => {
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
case actionTypes.DISMISS_TOAST: {
|
||||
const { toastId } = action
|
||||
const { toastId } = action;
|
||||
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -66,20 +65,20 @@ export const reducer = (state: State, action: Action): State => {
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
case actionTypes.REMOVE_TOAST:
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
import { State } from './types'
|
||||
import { State } from "./types";
|
||||
|
||||
// 전역 상태 및 리스너
|
||||
export const listeners: Array<(state: State) => void> = []
|
||||
export const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
// memoryState와 lastAction은 toastManager.ts에서 관리
|
||||
export { memoryState } from './toastManager';
|
||||
export { memoryState } from "./toastManager";
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
|
||||
import { Action, actionTypes } from './types'
|
||||
import { TOAST_LIMIT } from './constants'
|
||||
import { reducer } from './reducer'
|
||||
import { listeners } from './store'
|
||||
import { Action, actionTypes } from "./types";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { TOAST_LIMIT } from "./constants";
|
||||
import { reducer } from "./reducer";
|
||||
import { listeners } from "./store";
|
||||
|
||||
// 전역 상태 관리
|
||||
let memoryState = { toasts: [] };
|
||||
let lastAction = null;
|
||||
|
||||
// ID 생성기
|
||||
let count = 0
|
||||
let count = 0;
|
||||
|
||||
export function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
export function dispatch(action: Action) {
|
||||
// 마지막 액션 정보 추출
|
||||
let actionId: string | undefined = undefined;
|
||||
if ('toast' in action && action.toast) {
|
||||
if ("toast" in action && action.toast) {
|
||||
actionId = action.toast.id;
|
||||
} else if ('toastId' in action) {
|
||||
} else if ("toastId" in action) {
|
||||
actionId = action.toastId;
|
||||
}
|
||||
|
||||
|
||||
// 동일한 토스트에 대한 중복 액션 방지
|
||||
const now = Date.now();
|
||||
const isSameAction = lastAction &&
|
||||
lastAction.type === action.type &&
|
||||
((action.type === actionTypes.ADD_TOAST &&
|
||||
lastAction.time > now - 1000) || // ADD 액션은 1초 내 중복 방지
|
||||
(action.type !== actionTypes.ADD_TOAST &&
|
||||
actionId === lastAction.id &&
|
||||
lastAction.time > now - 300)); // 다른 액션은 300ms 내 중복 방지
|
||||
|
||||
const isSameAction =
|
||||
lastAction &&
|
||||
lastAction.type === action.type &&
|
||||
((action.type === actionTypes.ADD_TOAST && lastAction.time > now - 1000) || // ADD 액션은 1초 내 중복 방지
|
||||
(action.type !== actionTypes.ADD_TOAST &&
|
||||
actionId === lastAction.id &&
|
||||
lastAction.time > now - 300)); // 다른 액션은 300ms 내 중복 방지
|
||||
|
||||
if (isSameAction) {
|
||||
console.log('중복 토스트 액션 무시:', action.type);
|
||||
logger.info("중복 토스트 액션 무시:", action.type);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 액션 추적 업데이트
|
||||
lastAction = {
|
||||
type: action.type,
|
||||
id: actionId,
|
||||
time: now
|
||||
lastAction = {
|
||||
type: action.type,
|
||||
id: actionId,
|
||||
time: now,
|
||||
};
|
||||
|
||||
|
||||
// REMOVE_TOAST 액션 우선순위 높임
|
||||
if (action.type === actionTypes.REMOVE_TOAST) {
|
||||
// 즉시 처리
|
||||
@@ -56,7 +56,7 @@ export function dispatch(action: Action) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 실제 상태 업데이트 및 리스너 호출
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
export type ToasterToast = {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: React.ReactNode
|
||||
variant?: "default" | "destructive"
|
||||
duration?: number
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
variant?: "default" | "destructive";
|
||||
duration?: number;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export type ActionType = typeof actionTypes
|
||||
export type ActionType = typeof actionTypes;
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast> & { id: string }
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast> & { id: string };
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: string
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: string;
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: string
|
||||
}
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: string;
|
||||
};
|
||||
|
||||
export interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
export type Toast = Omit<ToasterToast, "id">
|
||||
export type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
Reference in New Issue
Block a user