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:
hansoo
2025-07-12 15:27:54 +09:00
parent 6a208d6b06
commit 9851627ff1
411 changed files with 14458 additions and 8680 deletions

View File

@@ -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에서

View File

@@ -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 };

View File

@@ -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;
}
}
};

View File

@@ -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";

View File

@@ -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) => {

View File

@@ -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">;