diff --git a/README.md b/README.md
new file mode 100644
index 00000000..8f94780e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,135 @@
+## 과제 체크포인트
+
+### 기본과제
+
+#### 1) 라우팅 구현:
+
+- [x] History API를 사용하여 SPA 라우터 구현
+ - [x] '/' (홈 페이지)
+ - [x] '/login' (로그인 페이지)
+ - [x] '/profile' (프로필 페이지)
+- [x] 각 라우트에 해당하는 컴포넌트 렌더링 함수 작성
+- [x] 네비게이션 이벤트 처리 (링크 클릭 시 페이지 전환)
+- [x] 주소가 변경되어도 새로고침이 발생하지 않아야 한다.
+
+#### 2) 사용자 관리 기능:
+
+- [x] LocalStorage를 사용한 간단한 사용자 데이터 관리
+ - [x] 사용자 정보 저장 (이름, 간단한 소개)
+ - [x] 로그인 상태 관리 (로그인/로그아웃 토글)
+- [x] 로그인 폼 구현
+ - [x] 사용자 이름 입력 및 검증
+ - [x] 로그인 버튼 클릭 시 LocalStorage에 사용자 정보 저장
+- [x] 로그아웃 기능 구현
+ - [x] 로그아웃 버튼 클릭 시 LocalStorage에서 사용자 정보 제거
+
+#### 3) 프로필 페이지 구현:
+
+- [x] 현재 로그인한 사용자의 정보 표시
+ - [x] 사용자 이름
+ - [x] 간단한 소개
+- [x] 프로필 수정 기능
+ - [x] 사용자 소개 텍스트 수정 가능
+ - [x] 수정된 정보 LocalStorage에 저장
+
+#### 4) 컴포넌트 기반 구조 설계:
+
+- [ ] 재사용 가능한 컴포넌트 작성
+ - [ ] Header 컴포넌트
+ - [ ] Footer 컴포넌트
+- [x] 페이지별 컴포넌트 작성
+ - [x] HomePage 컴포넌트
+ - [x] ProfilePage 컴포넌트
+ - [x] NotFoundPage 컴포넌트
+
+#### 5) 상태 관리 초기 구현:
+
+- [ ] 간단한 상태 관리 시스템 설계
+ - [ ] 전역 상태 객체 생성 (예: 현재 로그인한 사용자 정보)
+- [ ] 상태 변경 함수 구현
+ - [ ] 상태 업데이트 시 관련 컴포넌트 리렌더링
+
+#### 6) 이벤트 처리 및 DOM 조작:
+
+- [ ] 사용자 입력 처리 (로그인 폼, 프로필 수정 등)
+- [ ] 동적 컨텐츠 렌더링 (사용자 정보 표시, 페이지 전환 등)
+
+#### 7) 기본적인 에러 처리:
+
+- [ ] 잘못된 라우트 접근 시 404 페이지 표시
+- [ ] 로그인 실패 시 에러 메시지 표시
+
+### 심화과제
+
+#### 1) 해시 라우터 구현
+
+- [ ] location.hash를 이용하여 SPA 라우터 구현
+ - [ ] '/#/' (홈 페이지)
+ - [ ] '/#/login' (로그인 페이지)
+ - [ ] '/#/profile' (프로필 페이지)
+
+#### 2) 라우트 가드 구현
+
+- [ ] 로그인 상태에 따른 접근 제어
+- [ ] 비로그인 사용자의 특정 페이지 접근 시 로그인 페이지로 리다이렉션
+
+#### 3) 이벤트 위임
+
+- [ ] 이벤트 위임 방식으로 이벤트를 관리하고 있다.
+
+## 과제 셀프회고
+
+
+
+### 기술적 성장
+
+
+
+### 코드 품질
+
+
+
+### 학습 효과 분석
+
+
+
+### 과제 피드백
+
+
+
+## 리뷰 받고 싶은 내용
+
+
diff --git a/package-lock.json b/package-lock.json
index 8a563559..3ac04d56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,9 @@
"": {
"name": "front-4th-chapter1-1",
"version": "0.0.0",
+ "dependencies": {
+ "playwright": "^1.49.1"
+ },
"devDependencies": {
"@eslint/js": "^9.16.0",
"@playwright/test": "^1.49.1",
@@ -2932,7 +2935,6 @@
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
- "dev": true,
"dependencies": {
"playwright-core": "1.49.1"
},
@@ -2950,7 +2952,6 @@
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
- "dev": true,
"bin": {
"playwright-core": "cli.js"
},
@@ -2962,7 +2963,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
diff --git a/package.json b/package.json
index e535bf69..25dcfb3f 100644
--- a/package.json
+++ b/package.json
@@ -42,5 +42,8 @@
"prettier": "^3.4.2",
"vite": "^6.0.3",
"vitest": "^2.1.8"
+ },
+ "dependencies": {
+ "playwright": "^1.49.1"
}
}
diff --git a/src/components/Footer.js b/src/components/Footer.js
new file mode 100644
index 00000000..fb4fd659
--- /dev/null
+++ b/src/components/Footer.js
@@ -0,0 +1,13 @@
+export const Footer = () => {
+ const template = `
+
+ `;
+
+ const init = () => {
+ console.log("Footer init");
+ };
+
+ return { template, init };
+};
diff --git a/src/components/Header.js b/src/components/Header.js
new file mode 100644
index 00000000..fda00068
--- /dev/null
+++ b/src/components/Header.js
@@ -0,0 +1,71 @@
+import { router } from "../router/router.js";
+import { userStore } from "../store/userStore.js";
+
+const navItems = [
+ { label: "홈", path: "/" },
+ { label: "프로필", path: "/profile" },
+ { label: "로그아웃", path: "/", action: "logout", authRequired: true },
+ { label: "로그인", path: "/login", guestOnly: true },
+];
+
+export const Header = () => {
+ const getNavItems = () => {
+ const isLoggedIn = userStore.isLogin();
+ return navItems.filter((item) => {
+ if (item.authRequired && !isLoggedIn) return false;
+ if (item.guestOnly && isLoggedIn) return false;
+ return true;
+ });
+ };
+
+ const template = `
+
+
+ `;
+
+ const init = () => {
+ const nav = document.querySelector("nav");
+ if (nav) {
+ nav.addEventListener("click", (e) => {
+ if (e.target.tagName === "A") {
+ e.preventDefault();
+ const path = e.target.getAttribute("href");
+ const action = e.target.dataset.action;
+
+ if (action === "logout") {
+ userStore.logout();
+ router.navigate("/");
+ return;
+ }
+
+ if (path === "/profile" && !userStore.isLogin()) {
+ alert("로그인이 필요한 서비스입니다.");
+ router.navigate("/login");
+ return;
+ }
+
+ router.navigate(path);
+ }
+ });
+ }
+ };
+
+ return { template, init };
+};
diff --git a/src/components/index.js b/src/components/index.js
new file mode 100644
index 00000000..758528c0
--- /dev/null
+++ b/src/components/index.js
@@ -0,0 +1,2 @@
+export * from "./Header.js";
+export * from "./Footer.js";
diff --git a/src/main.js b/src/main.js
index 036c2a38..c1d42230 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,241 +1,5 @@
-const MainPage = () => `
-
-
-
+import { router } from "./router/router.js";
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
오늘 날씨가 정말 좋네요. 다들 좋은 하루 보내세요!
-
-
-
-
-
-
-
-
-
-
-
-
-
새로운 프로젝트를 시작했어요. 열심히 코딩 중입니다!
-
-
-
-
-
-
-
-
-
-
-
-
-
오늘 점심 메뉴 추천 받습니다. 뭐가 좋을까요?
-
-
-
-
-
-
-
-
-
-
-
-
-
주말에 등산 가실 분 계신가요? 함께 가요!
-
-
-
-
-
-
-
-
-
-
-
-
-
새로 나온 영화 재미있대요. 같이 보러 갈 사람?
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-const ErrorPage = () => `
-
-
-
항해플러스
-
404
-
페이지를 찾을 수 없습니다
-
- 요청하신 페이지가 존재하지 않거나 이동되었을 수 있습니다.
-
-
- 홈으로 돌아가기
-
-
-
-`;
-
-const LoginPage = () => `
-
-
-
항해플러스
-
-
-
-
-
-
-
-
-`;
-
-const ProfilePage = () => `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-document.body.innerHTML = `
- ${MainPage()}
- ${ProfilePage()}
- ${LoginPage()}
- ${ErrorPage()}
-`;
+window.addEventListener("load", () => {
+ router.render();
+});
diff --git a/src/pages/ErrorPage.js b/src/pages/ErrorPage.js
new file mode 100644
index 00000000..b37f2d46
--- /dev/null
+++ b/src/pages/ErrorPage.js
@@ -0,0 +1,23 @@
+export const ErrorPage = () => {
+ const template = `
+
+
+
항해플러스
+
404
+
페이지를 찾을 수 없습니다
+
+ 요청하신 페이지가 존재하지 않거나 이동되었을 수 있습니다.
+
+
+ 홈으로 돌아가기
+
+
+
+ `;
+
+ const init = () => {
+ console.log("ErrorPage init");
+ };
+
+ return { template, init };
+};
diff --git a/src/pages/LoginPage.js b/src/pages/LoginPage.js
new file mode 100644
index 00000000..91d68bc1
--- /dev/null
+++ b/src/pages/LoginPage.js
@@ -0,0 +1,49 @@
+import { userStore } from "../store/userStore.js";
+
+export const LoginPage = () => {
+ const template = `
+
+
+
항해플러스
+
+
+
+
+
+
+
+
+ `;
+
+ const init = () => {
+ const loginForm = document.getElementById("login-form");
+
+ if (loginForm) {
+ loginForm.addEventListener("submit", (event) => {
+ event.preventDefault();
+ const username = document.getElementById("username").value.trim();
+ const password = document.getElementById("password").value.trim();
+
+ if (username && password) {
+ userStore.login({ username });
+ alert("로그인 성공");
+ window.location.href = "/profile";
+ } else {
+ alert("유효한 사용자 이름과 비밀번호를 입력하세요.");
+ }
+ });
+ }
+ };
+
+ return { template, init };
+};
diff --git a/src/pages/MainPage.js b/src/pages/MainPage.js
new file mode 100644
index 00000000..893970e8
--- /dev/null
+++ b/src/pages/MainPage.js
@@ -0,0 +1,109 @@
+import { Header, Footer } from "../components/index.js";
+
+export const MainPage = () => {
+ const header = Header();
+ const footer = Footer();
+ const template = `
+
+
+ ${header.template}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
오늘 날씨가 정말 좋네요. 다들 좋은 하루 보내세요!
+
+
+
+
+
+
+
+
+
+
+
+
+
새로운 프로젝트를 시작했어요. 열심히 코딩 중입니다!
+
+
+
+
+
+
+
+
+
+
+
+
+
오늘 점심 메뉴 추천 받습니다. 뭐가 좋을까요?
+
+
+
+
+
+
+
+
+
+
+
+
+
주말에 등산 가실 분 계신가요? 함께 가요!
+
+
+
+
+
+
+
+
+
+
+
+
+
새로 나온 영화 재미있대요. 같이 보러 갈 사람?
+
+
+
+
+
+
+
+
+ ${footer.template}
+
+
+ `;
+ const init = () => {
+ console.log("MainPage init");
+ header.init();
+ footer.init();
+ };
+ return { template, init };
+};
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
new file mode 100644
index 00000000..306a3b52
--- /dev/null
+++ b/src/pages/ProfilePage.js
@@ -0,0 +1,117 @@
+import { Header, Footer } from "../components/index.js";
+import { userStore } from "../store/userStore.js";
+import { router } from "../router/router.js";
+
+export const ProfilePage = () => {
+ const header = Header();
+ const footer = Footer();
+ const template = `
+
+
+
+ ${header.template}
+
+
+
+ 내 프로필
+
+
+
+
+ ${footer.template}
+
+
+
+ `;
+
+ const init = () => {
+ header.init();
+ footer.init();
+
+ const form = document.getElementById("profile-form");
+ const usernameInput = document.getElementById("username");
+ const emailInput = document.getElementById("email");
+ const bioTextarea = document.getElementById("bio");
+
+ // 초기값 설정
+ const user = userStore.getUser();
+ if (user) {
+ usernameInput.value = user.username || "";
+ emailInput.value = user.email || "";
+ bioTextarea.value = user.bio || "";
+ }
+
+ // 폼 제출 처리
+ form.addEventListener("submit", (e) => {
+ e.preventDefault();
+
+ const updatedUser = {
+ username: usernameInput.value.trim(),
+ email: emailInput.value.trim(),
+ bio: bioTextarea.value.trim(),
+ };
+
+ // 유효성 검사
+ if (!updatedUser.username || !updatedUser.email) {
+ alert("사용자 이름과 이메일은 필수 항목입니다.");
+ return;
+ }
+
+ // 사용자 정보 업데이트
+ userStore.saveUserToStorage(updatedUser);
+ alert("프로필이 성공적으로 업데이트되었습니다!");
+ router.navigate("/");
+ });
+ };
+
+ return { template, init };
+};
diff --git a/src/pages/index.js b/src/pages/index.js
new file mode 100644
index 00000000..bd8260af
--- /dev/null
+++ b/src/pages/index.js
@@ -0,0 +1,4 @@
+export * from "./ErrorPage";
+export * from "./LoginPage";
+export * from "./MainPage";
+export * from "./ProfilePage";
diff --git a/src/router/Router.js b/src/router/Router.js
new file mode 100644
index 00000000..3ec26030
--- /dev/null
+++ b/src/router/Router.js
@@ -0,0 +1,60 @@
+import { MainPage, ProfilePage, LoginPage, ErrorPage } from "../pages";
+import { attachProfileFormListeners } from "../pages/ProfilePage";
+
+export class Router {
+ constructor() {
+ this.routes = {};
+ window.addEventListener("popstate", () => this.handlePopState());
+ }
+
+ addRoute(path, handler) {
+ this.routes[path] = handler;
+ }
+
+ navigateTo(path) {
+ history.pushState(null, "", path);
+ this.handleRoute(path);
+ }
+
+ handlePopState() {
+ this.handleRoute(window.location.pathname);
+ }
+
+ handleRoute(path) {
+ const handler = this.routes[path];
+ const appRoot = document.getElementById("root");
+
+ if (handler && appRoot) {
+ appRoot.innerHTML = handler();
+
+ // 각 페이지 렌더링 후 필요한 작업 수행
+ if (path === "/profile") {
+ attachProfileFormListeners(); // 프로필 페이지에 이벤트 리스너 추가
+ }
+ } else if (appRoot) {
+ appRoot.innerHTML = ErrorPage(); // 404 페이지 처리
+ }
+ }
+ beforeNavigate(path) {
+ const userId = localStorage.getItem("userId");
+
+ // 비로그인 상태에서 /profile 접근 시 로그인 페이지로 리다이렉트
+ if (path === "/profile" && !userId) {
+ this.navigateTo("/login");
+ return false; // 현재 경로를 중단
+ }
+ return true;
+ }
+}
+
+export const router = new Router();
+
+const routes = [
+ { path: "/", handler: MainPage },
+ { path: "/profile", handler: ProfilePage },
+ { path: "/login", handler: LoginPage },
+];
+
+routes.forEach((route) => {
+ router.addRoute(route.path, route.handler);
+});
diff --git a/src/router/index.js b/src/router/index.js
new file mode 100644
index 00000000..b9e37734
--- /dev/null
+++ b/src/router/index.js
@@ -0,0 +1 @@
+export * from "./router.js";
diff --git a/src/router/router.js b/src/router/router.js
new file mode 100644
index 00000000..10f127ed
--- /dev/null
+++ b/src/router/router.js
@@ -0,0 +1,39 @@
+import { MainPage, LoginPage, ProfilePage, ErrorPage } from "../pages";
+
+export class Router {
+ constructor(routes) {
+ this.routes = routes;
+ this.currentRoute = window.location.pathname;
+ this.init();
+ }
+
+ init() {
+ window.addEventListener("popstate", () => {
+ this.currentRoute = window.location.pathname;
+ this.render();
+ });
+ this.render();
+ }
+
+ navigate(path) {
+ this.currentRoute = path;
+ history.pushState(null, "", path);
+ this.render();
+ }
+
+ render() {
+ const route = this.routes[this.currentRoute] || this.routes["/NotFound"];
+ const { template, init } = route();
+ document.body.innerHTML = template;
+ if (typeof init === "function") init();
+ }
+}
+
+const routes = {
+ "/": MainPage,
+ "/login": LoginPage,
+ "/profile": ProfilePage,
+ "/NotFound": ErrorPage,
+};
+
+export const router = new Router(routes);
diff --git a/src/store/userStore.js b/src/store/userStore.js
new file mode 100644
index 00000000..b92689d8
--- /dev/null
+++ b/src/store/userStore.js
@@ -0,0 +1,43 @@
+class UserStore {
+ constructor() {
+ this.user = this.getUserFromStorage();
+ this.isLoggedIn = !!this.user;
+ }
+
+ getUserFromStorage() {
+ const user = localStorage.getItem("user");
+ return user ? JSON.parse(user) : null;
+ }
+ isLogin() {
+ return this.isLoggedIn;
+ }
+ saveUserToStorage(user) {
+ localStorage.setItem("user", JSON.stringify(user));
+ this.user = user;
+ this.isLoggedIn = true;
+ }
+
+ clearUserFromStorage() {
+ localStorage.removeItem("user");
+ this.user = null;
+ this.isLoggedIn = false;
+ }
+
+ login(user) {
+ this.saveUserToStorage(user);
+ }
+
+ logout() {
+ this.clearUserFromStorage();
+ }
+
+ getUser() {
+ return this.user;
+ }
+
+ isAuthenticated() {
+ return this.isLoggedIn;
+ }
+}
+
+export const userStore = new UserStore();