+ {/* 데스크탑 */}
+
+
+ 2026 23RD CEOS AWARDS
+
+
+ {NAV_ITEMS.map((item) => (
+ {
+ e.preventDefault();
+ guardedRouter.push(item.href);
+ }}
+ className={
+ pathname === item.href ? "text-blue-500" : "text-white"
+ }
+ >
+ {item.label}
+
+ ))}
+ {isLoggedIn ? (
+
+ ) : (
+
+ LOGIN
+
+ )}
+
+
+
+ {/* 모바일 */}
+
+
+ CEOS
+
+ AWARDS
+
+
+
+
+ {open && (
+
+
setOpen(false)} />
+
+
+ {NAV_ITEMS.map((item) => (
+ {
+ e.preventDefault();
+ setOpen(false);
+ guardedRouter.push(item.href);
+ }}
+ className={`text-2xl font-bold ${pathname === item.href ? "text-blue-500" : "text-white"}`}
+ >
+ {item.label}
+
+ ))}
+ {isLoggedIn ? (
+
+ ) : (
+ setOpen(false)}
+ className={`text-2xl font-bold ${pathname === "/login" ? "text-blue-500" : "text-white"}`}
+ >
+ LOGIN
+
+ )}
+
+
+ )}
+
+ {showLoginModal && (
+
setShowLoginModal(false)}
+ redirectTo={blockedPath}
+ />
+ )}
+
+ );
+}
diff --git a/src/components/RankBadge.tsx b/src/components/RankBadge.tsx
new file mode 100644
index 0000000..7f24734
--- /dev/null
+++ b/src/components/RankBadge.tsx
@@ -0,0 +1,18 @@
+type Props = {
+ rank: number;
+ color: string;
+};
+
+export default function RankBadge({ rank, color }: Props) {
+ return (
+
+ );
+}
diff --git a/src/components/VoteCount.tsx b/src/components/VoteCount.tsx
new file mode 100644
index 0000000..535878d
--- /dev/null
+++ b/src/components/VoteCount.tsx
@@ -0,0 +1,16 @@
+type Props = {
+ name: string;
+ votes: number;
+ color: string;
+};
+
+export default function VoteCount({ name, votes, color }: Props) {
+ return (
+
+ {name} | {votes}표
+
+ );
+}
diff --git a/src/constants/endpoint.ts b/src/constants/endpoint.ts
new file mode 100644
index 0000000..90254bc
--- /dev/null
+++ b/src/constants/endpoint.ts
@@ -0,0 +1,19 @@
+export const API_ENDPOINTS = {
+ AUTH: {
+ SIGNUP: "/auth/signup",
+ LOGIN: "/auth/login",
+ REISSUE: "/auth/reissue",
+ LOGOUT: "/auth/logout",
+ },
+
+ CANDIDATES: "/candidates",
+
+ VOTES: {
+ PART_LEADER: "/votes/part-leader",
+ PART_LEADER_CANDIDATES: "/votes/part-leader/candidates",
+ PART_LEADER_RESULT: "/votes/part-leader/result",
+ DEMO_DAY: "/votes/demo-day",
+ DEMO_DAY_CANDIDATES: "/votes/demo-day/candidates",
+ DEMO_DAY_RESULT: "/votes/demo-day/result",
+ },
+} as const;
diff --git a/src/constants/members.ts b/src/constants/members.ts
new file mode 100644
index 0000000..56c213e
--- /dev/null
+++ b/src/constants/members.ts
@@ -0,0 +1,83 @@
+export type Part = "PM" | "DESIGN" | "FRONTEND" | "BACKEND";
+
+export interface Member {
+ name: string;
+ isExecutive: boolean;
+ isLeader: boolean;
+ school?: string;
+ department?: string;
+}
+
+export const MEMBERS: Record
= {
+ PM: [
+ { name: "김채원", isExecutive: false, isLeader: false },
+ { name: "문현승", isExecutive: false, isLeader: false },
+ { name: "변성우", isExecutive: false, isLeader: false },
+ { name: "안민용", isExecutive: false, isLeader: false },
+ { name: "안서연", isExecutive: false, isLeader: false },
+ { name: "안세빈", isExecutive: false, isLeader: false },
+ { name: "오유준", isExecutive: false, isLeader: false },
+ { name: "이소은", isExecutive: false, isLeader: false },
+ { name: "이정원", isExecutive: false, isLeader: false },
+ { name: "조아현", isExecutive: false, isLeader: false },
+ { name: "현종혁", isExecutive: true, isLeader: true },
+ { name: "박준영", isExecutive: true, isLeader: false },
+ { name: "이우혁", isExecutive: true, isLeader: false },
+ { name: "이혜린", isExecutive: true, isLeader: false },
+ { name: "조은호", isExecutive: true, isLeader: false },
+ { name: "허유진", isExecutive: true, isLeader: false },
+ ],
+ DESIGN: [
+ { name: "강예린", isExecutive: false, isLeader: false },
+ { name: "고다현", isExecutive: false, isLeader: false },
+ { name: "권지민", isExecutive: false, isLeader: false },
+ { name: "김미소", isExecutive: false, isLeader: false },
+ { name: "김은홍", isExecutive: false, isLeader: false },
+ { name: "김정원", isExecutive: false, isLeader: false },
+ { name: "문수인", isExecutive: false, isLeader: false },
+ { name: "오상헌", isExecutive: false, isLeader: false },
+ { name: "우유민", isExecutive: false, isLeader: false },
+ { name: "이우림", isExecutive: false, isLeader: false },
+ { name: "박서령", isExecutive: true, isLeader: true },
+ { name: "노성주", isExecutive: true, isLeader: false },
+ { name: "성유정", isExecutive: true, isLeader: false },
+ { name: "윤시연", isExecutive: true, isLeader: false },
+ { name: "정시빈", isExecutive: true, isLeader: false },
+ { name: "정은선", isExecutive: true, isLeader: false },
+ { name: "천영현", isExecutive: true, isLeader: false },
+ ],
+ FRONTEND: [
+ { name: "구민교", isExecutive: false, isLeader: false },
+ { name: "권오진", isExecutive: false, isLeader: false },
+ { name: "김민서", isExecutive: false, isLeader: false },
+ { name: "김홍엽", isExecutive: false, isLeader: false },
+ { name: "남기림", isExecutive: false, isLeader: false },
+ { name: "박유민", isExecutive: false, isLeader: false },
+ { name: "오유진", isExecutive: false, isLeader: false },
+ { name: "이승연", isExecutive: false, isLeader: false },
+ { name: "이윤서", isExecutive: false, isLeader: false },
+ { name: "황영준", isExecutive: false, isLeader: false },
+ { name: "원채영", isExecutive: true, isLeader: true },
+ { name: "김류원", isExecutive: true, isLeader: false },
+ { name: "김윤성", isExecutive: true, isLeader: false },
+ { name: "손주완", isExecutive: true, isLeader: false },
+ ],
+ BACKEND: [
+ { name: "김도현", isExecutive: false, isLeader: false },
+ { name: "김동욱", isExecutive: false, isLeader: false },
+ { name: "김태익", isExecutive: false, isLeader: false },
+ { name: "김태희", isExecutive: false, isLeader: false },
+ { name: "안준석", isExecutive: false, isLeader: false },
+ { name: "오지송", isExecutive: false, isLeader: false },
+ { name: "임종훈", isExecutive: false, isLeader: false },
+ { name: "최승원", isExecutive: false, isLeader: false },
+ { name: "최우혁", isExecutive: false, isLeader: false },
+ { name: "황신애", isExecutive: false, isLeader: false },
+ { name: "변호영", isExecutive: true, isLeader: true },
+ { name: "신 혁", isExecutive: true, isLeader: false },
+ { name: "이수아", isExecutive: true, isLeader: false },
+ { name: "이연호", isExecutive: true, isLeader: false },
+ { name: "이윤지", isExecutive: true, isLeader: false },
+ { name: "이준영", isExecutive: true, isLeader: false },
+ ],
+};
diff --git a/src/constants/teams.ts b/src/constants/teams.ts
new file mode 100644
index 0000000..99f06c5
--- /dev/null
+++ b/src/constants/teams.ts
@@ -0,0 +1,28 @@
+export type Part = "frontend" | "backend";
+
+export type TeamName = "Ditda" | "JobDri" | "Groupeat" | "IPX" | "CONX";
+
+export const TEAM_NAMES: TeamName[] = [
+ "Ditda",
+ "JobDri",
+ "Groupeat",
+ "IPX",
+ "CONX",
+];
+
+export const TEAM_MEMBERS: Record> = {
+ frontend: {
+ Ditda: ["박유민", "권오진"],
+ JobDri: ["이윤서", "구민교"],
+ Groupeat: ["이승연", "황영준"],
+ IPX: ["남기림", "김민서"],
+ CONX: ["김홍엽", "오유진"],
+ },
+ backend: {
+ Ditda: ["임종훈", "안준석"],
+ JobDri: ["황신애", "최우혁"],
+ Groupeat: ["김동욱", "최승원"],
+ IPX: ["오지송", "김태익"],
+ CONX: ["김태희", "김도현"],
+ },
+};
diff --git a/src/lib/api.ts b/src/lib/api.ts
new file mode 100644
index 0000000..2563f47
--- /dev/null
+++ b/src/lib/api.ts
@@ -0,0 +1,55 @@
+import axios from "axios";
+import { useAuthStore } from "@/store/authStore";
+import { API_ENDPOINTS } from "@/constants/endpoint";
+
+//맨처음에 백엔드 api 자겨오는 부분
+export const api = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
+ withCredentials: true,
+});
+
+//api 인터셉터 부분
+api.interceptors.request.use((config) => {
+ const token = useAuthStore.getState().accessToken;
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+});
+
+api.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ const originalRequest = error.config;
+
+ const skipReissue = [API_ENDPOINTS.AUTH.LOGIN, API_ENDPOINTS.AUTH.REISSUE].some(
+ (path) => originalRequest.url?.includes(path),
+ );
+
+ if (error.response?.status === 401 && !originalRequest._retry && !skipReissue) {
+ originalRequest._retry = true;
+
+ try {
+ const { data } = await axios.post(
+ `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}${API_ENDPOINTS.AUTH.REISSUE}`,
+ {},
+ { withCredentials: true },
+ );
+ const newToken = data.data.accessToken;
+ useAuthStore.getState().setAccessToken(newToken);
+ originalRequest.headers.Authorization = `Bearer ${newToken}`;
+
+ return api(originalRequest);
+ } catch (reissueError) {
+ useAuthStore.getState().clearAuth();
+ const code =
+ axios.isAxiosError(reissueError)
+ ? (reissueError.response?.data?.error?.code ?? "session")
+ : "session";
+ window.location.href = `/login?reason=${code}`;
+ }
+ }
+
+ return Promise.reject(error);
+ },
+);
diff --git a/src/schemas/login.ts b/src/schemas/login.ts
new file mode 100644
index 0000000..6ad6023
--- /dev/null
+++ b/src/schemas/login.ts
@@ -0,0 +1,8 @@
+import { z } from "zod";
+
+export const loginSchema = z.object({
+ username: z.string().min(1, "아이디를 입력해 주세요"),
+ password: z.string().min(1, "비밀번호를 입력해 주세요"),
+});
+
+export type LoginForm = z.infer;
diff --git a/src/schemas/signup.ts b/src/schemas/signup.ts
new file mode 100644
index 0000000..6e97b40
--- /dev/null
+++ b/src/schemas/signup.ts
@@ -0,0 +1,18 @@
+import { z } from "zod";
+
+export const signupSchema = z
+ .object({
+ part: z.enum(["frontend", "backend"]),
+ team: z.string().min(1),
+ member: z.string().min(1),
+ username: z.string().min(1),
+ email: z.email("이메일 형식이 올바르지 않습니다"),
+ password: z.string().min(1),
+ passwordRe: z.string().min(1),
+ })
+ .refine((data) => data.password === data.passwordRe, {
+ message: "비밀번호가 일치하지 않습니다",
+ path: ["passwordRe"],
+ });
+
+export type SignupForm = z.infer;
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
new file mode 100644
index 0000000..cd56ba0
--- /dev/null
+++ b/src/store/authStore.ts
@@ -0,0 +1,25 @@
+import { create } from "zustand";
+
+type User = {
+ userId: number;
+ username: string;
+ name: string;
+ part: string;
+ team: string;
+};
+
+type AuthState = {
+ accessToken: string | null;
+ user: User | null;
+ setAuth: (accessToken: string, user: User) => void;
+ setAccessToken: (accessToken: string) => void;
+ clearAuth: () => void;
+};
+
+export const useAuthStore = create((set) => ({
+ accessToken: null,
+ user: null,
+ setAuth: (accessToken, user) => set({ accessToken, user }),
+ setAccessToken: (accessToken) => set({ accessToken }),
+ clearAuth: () => set({ accessToken: null, user: null }),
+}));
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cf9c65d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}