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