diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e9ee7e78 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Type check + run: npx tsc --noEmit + + - name: Build + run: npm run build + env: + NODE_ENV: production + diff --git a/src/app/login/LoginContent.tsx b/src/app/login/LoginContent.tsx index 729c2694..7cacede7 100644 --- a/src/app/login/LoginContent.tsx +++ b/src/app/login/LoginContent.tsx @@ -84,7 +84,7 @@ const LoginContent = () => { { diff --git a/src/app/sign-up/email/EmailSignUpForm.tsx b/src/app/sign-up/email/EmailSignUpForm.tsx index 69831bd0..05674585 100644 --- a/src/app/sign-up/email/EmailSignUpForm.tsx +++ b/src/app/sign-up/email/EmailSignUpForm.tsx @@ -73,8 +73,9 @@ const EmailSignUpForm = () => { onSuccess: (data) => { router.push(`/sign-up?token=${data.signUpToken}`); }, - onError: (error: any) => { - toast.error(error.response?.data?.message || "회원가입에 실패했습니다."); + onError: (error: unknown) => { + const axiosError = error as { response?: { data?: { message?: string } } }; + toast.error(axiosError.response?.data?.message || "회원가입에 실패했습니다."); }, }, ); diff --git a/src/app/university/score/submit/language-test/_lib/schema.ts b/src/app/university/score/submit/language-test/_lib/schema.ts index 2a9b21b5..1bb25974 100644 --- a/src/app/university/score/submit/language-test/_lib/schema.ts +++ b/src/app/university/score/submit/language-test/_lib/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { validateLanguageScore } from "@/utils/scoreUtils"; +import validateLanguageScore from "@/utils/scoreUtils"; import { LanguageTestEnum } from "@/types/score"; diff --git a/src/components/button/BlockBtn.tsx b/src/components/button/BlockBtn.tsx index 13d9563d..5b6fbdcf 100644 --- a/src/components/button/BlockBtn.tsx +++ b/src/components/button/BlockBtn.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { type VariantProps, cva } from "class-variance-authority"; -import { cn } from "@/utils/designUtils"; +import cn from "@/utils/designUtils"; const blockBtnVariants = cva("h-13 w-full min-w-80 max-w-screen-sm rounded-lg flex items-center justify-center", { variants: { diff --git a/src/components/button/RoundBtn.tsx b/src/components/button/RoundBtn.tsx index 6885662b..a2f8027c 100644 --- a/src/components/button/RoundBtn.tsx +++ b/src/components/button/RoundBtn.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { type VariantProps, cva } from "class-variance-authority"; -import { cn } from "@/utils/designUtils"; +import cn from "@/utils/designUtils"; const roundBtnVariants = cva("h-[2.375rem] w-[6.375rem] rounded-3xl px-4 py-2.5 ", { variants: { diff --git a/src/components/login/signup/SignupPolicyScreen.tsx b/src/components/login/signup/SignupPolicyScreen.tsx index 3154bc4c..07b83a7d 100644 --- a/src/components/login/signup/SignupPolicyScreen.tsx +++ b/src/components/login/signup/SignupPolicyScreen.tsx @@ -58,10 +58,9 @@ const SignupPolicyScreen = ({ toNextStage }: SignupPolicyScreenProps) => {
다음 diff --git a/src/components/login/signup/SignupSurvey.tsx b/src/components/login/signup/SignupSurvey.tsx index 59fe1587..e3265da0 100644 --- a/src/components/login/signup/SignupSurvey.tsx +++ b/src/components/login/signup/SignupSurvey.tsx @@ -64,8 +64,9 @@ const SignupSurvey = ({ baseNickname, baseEmail, baseProfileImageUrl }: SignupSu try { const result = await uploadImageMutation.mutateAsync(profileImageFile); imageUrl = result.fileUrl; - } catch (err: any) { - console.error("Error", err.message); + } catch (err: unknown) { + const error = err as { message?: string }; + console.error("Error", error.message); // toast.error는 hook의 onError에서 이미 처리되므로 중복 호출 제거 } } @@ -89,19 +90,24 @@ const SignupSurvey = ({ baseNickname, baseEmail, baseProfileImageUrl }: SignupSu toast.success("회원가입이 완료되었습니다."); router.push("/"); }, - onError: (error: any) => { - if (error.response) { - console.error("Axios response error", error.response); - toast.error(error.response.data?.message || "회원가입에 실패했습니다."); + onError: (error: unknown) => { + const axiosError = error as { + response?: { data?: { message?: string } }; + message?: string; + }; + if (axiosError.response) { + console.error("Axios response error", axiosError.response); + toast.error(axiosError.response.data?.message || "회원가입에 실패했습니다."); } else { - console.error("Error", error.message); - toast.error(error.message || "회원가입에 실패했습니다."); + console.error("Error", axiosError.message); + toast.error(axiosError.message || "회원가입에 실패했습니다."); } }, }); - } catch (err: any) { - console.error("Error", err.message); - toast.error(err.message || "회원가입에 실패했습니다."); + } catch (err: unknown) { + const error = err as { message?: string }; + console.error("Error", error.message); + toast.error(error.message || "회원가입에 실패했습니다."); } }; diff --git a/src/components/search/UniversityFilterSection.tsx b/src/components/search/UniversityFilterSection.tsx index 46d76b0f..c9996c13 100644 --- a/src/components/search/UniversityFilterSection.tsx +++ b/src/components/search/UniversityFilterSection.tsx @@ -9,7 +9,7 @@ import UniversitySearchInput from "@/components/search/UniversitySearchInput"; import { RegionKo } from "@/types/university"; -import { RegionOption } from "@/app/search/SearchContent"; +import { RegionOption } from "@/app/search/SearchContent.tsx"; import { IconDownArrow, IconHatColor, IconHatGray, IconLocationColor, IconLocationGray } from "@/public/svgs/search"; interface UniversityFilterSectionProps { diff --git a/src/components/search/UniversityRegionTabs.tsx b/src/components/search/UniversityRegionTabs.tsx index 41a00841..71dc50d5 100644 --- a/src/components/search/UniversityRegionTabs.tsx +++ b/src/components/search/UniversityRegionTabs.tsx @@ -4,7 +4,7 @@ import React from "react"; import { RegionKo } from "@/types/university"; -import { RegionOption } from "@/app/search/SearchContent"; +import { RegionOption } from "@/app/search/SearchContent.tsx"; interface UniversityRegionTabsProps { regions: RegionOption[]; diff --git a/src/components/ui/UniverSityCard/index.tsx b/src/components/ui/UniverSityCard/index.tsx index e93d2512..1d7ec1cc 100644 --- a/src/components/ui/UniverSityCard/index.tsx +++ b/src/components/ui/UniverSityCard/index.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; import { convertImageUrl } from "@/utils/fileUtils"; -import { shortenLanguageTestName } from "@/utils/universityUtils"; +import shortenLanguageTestName from "@/utils/universityUtils"; import CheveronRightFilled from "@/components/ui/icon/ChevronRightFilled"; diff --git a/src/utils/designUtils.ts b/src/utils/designUtils.ts index e167c34a..27692d34 100644 --- a/src/utils/designUtils.ts +++ b/src/utils/designUtils.ts @@ -1,4 +1,6 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; -export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); +const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); + +export default cn; diff --git a/src/utils/jwtUtils.ts b/src/utils/jwtUtils.ts index a732c1e4..43cfbae1 100644 --- a/src/utils/jwtUtils.ts +++ b/src/utils/jwtUtils.ts @@ -1,7 +1,14 @@ +interface JwtPayload { + sub: number; + role: string; + iat: number; + exp: number; +} + export const isTokenExpired = (token: string | null): boolean => { if (!token) return true; try { - const payload = JSON.parse(atob(token.split(".")[1])); + const payload = JSON.parse(atob(token.split(".")[1])) as JwtPayload; const currentTime = Math.floor(Date.now() / 1000); return payload.exp < currentTime; } catch (error) { @@ -10,20 +17,13 @@ export const isTokenExpired = (token: string | null): boolean => { } }; -interface JwtPayload { - sub: number; - role: string; - iat: number; - exp: number; -} - export const tokenParse = (token: string | null): JwtPayload | null => { if (typeof window === "undefined") return null; if (!token) return null; try { - const payload: JwtPayload = JSON.parse(atob(token.split(".")[1])); + const payload = JSON.parse(atob(token.split(".")[1])) as JwtPayload; return payload; } catch (error) { console.error("토큰 파싱 오류:", error); diff --git a/src/utils/scoreUtils.ts b/src/utils/scoreUtils.ts index 020109ec..7848e0eb 100644 --- a/src/utils/scoreUtils.ts +++ b/src/utils/scoreUtils.ts @@ -1,6 +1,6 @@ import { LanguageTestEnum } from "@/types/score"; -export const validateLanguageScore = (testType: string, score: string) => { +const validateLanguageScore = (testType: string, score: string) => { const numScore = Number(score); if (testType === LanguageTestEnum.TOEIC) { @@ -31,3 +31,5 @@ export const validateLanguageScore = (testType: string, score: string) => { } } }; + +export default validateLanguageScore; diff --git a/src/utils/universityUtils.ts b/src/utils/universityUtils.ts index fa04e361..ca288534 100644 --- a/src/utils/universityUtils.ts +++ b/src/utils/universityUtils.ts @@ -1,7 +1,10 @@ import { SHORT_LANGUAGE_TEST } from "@/constants/application"; -export const shortenLanguageTestName = (name: string) => { +const shortenLanguageTestName = (name: string): string | undefined => { if (Object.prototype.hasOwnProperty.call(SHORT_LANGUAGE_TEST, name)) { - return SHORT_LANGUAGE_TEST[name]; + return SHORT_LANGUAGE_TEST[name] as string; } + return undefined; }; + +export default shortenLanguageTestName; diff --git a/src/utils/useInfinityScroll.ts b/src/utils/useInfinityScroll.ts index 79fa49bc..78c0af18 100644 --- a/src/utils/useInfinityScroll.ts +++ b/src/utils/useInfinityScroll.ts @@ -7,7 +7,7 @@ type UseInfinityScrollProps = { }; type UseInfinityScrollReturn = { - lastElementRef: (node: HTMLDivElement | null) => void; + lastElementRef: (node: Element | null) => void; }; const useInfinityScroll = ({ @@ -25,7 +25,8 @@ const useInfinityScroll = ({ if (!node) return; observerRef.current = new IntersectionObserver( - ([entry]) => { + (entries) => { + const [entry] = entries; if (entry.isIntersecting) { fetchNextPage(); }