diff --git a/README.md b/README.md
index 39e30e9..ab1eef2 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,3 @@
# Weekly-Mission
-## Week 3
-
-### 필수 요구사항
-
-#### 전체
-
-- [x] 로고 클릭시 루트 페이지(“/”)로 이동해야 합니다.
-- [x] 로그인 페이지, 회원가입 페이지 모두 로고위 상단 여백이 동일해야 합니다.
-- [x] input 요소에 focus in 일 때, input 안에 문자열은 검정색이어야 하고, 파랑색 테두리가 생겨야 합니다.
-- [x] input 요소에 focus out 일 때, input 안에 문자열과 테두리는 회색이어야 합니다.
-- [x] SNS 아이콘들은 클릭 가능함을 확인할 수 있고, 클릭시 각각 “https://www.google.com/”, “https://www.kakaocorp.com/page/” 으로 이동합니다.
-
-#### 로그인페이지
-
-- [x] 회원 가입하기는 클릭 가능함을 확인할 수 있고, 클릭시 “/signup” 페이지로 이동합니다.
-- [x] 이메일 input에서 focus out 일 때, 값이 없을 경우 alert으로 “이메일을 입력해주세요.” 메세지를 보입니다.
-- [x] 이메일 input에서 focus out 일 때, 값이 있고, 이메일 형식에 맞지 않을 경우 alert으로 “올바른 이메일 주소가 아닙니다.” 메세지를 보입니다.
-- [x] 이메일: test@codeit.com, 비밀번호: codeit101 으로 로그인 시도할 경우, “/my-link” 페이지로 이동합니다.
-- [x] 이외의 로그인 시도의 경우, “이메일과 비밀번호를 확인해주세요.” 메세지가 담긴 alert 을 띄워 주세요.
-- [x] 로그인 버튼 클릭 또는 Enter키 입력으로 로그인 실행돼야 합니다.
-- [x] 비밀번호 찾기는 클릭 가능함을 확인할 수 있고, 클릭시 “/forgot-password” 페이지로 이동합니다.
-
-#### 회원가입 페이지
-
-- [x] 로그인 하기는 클릭 가능함을 확인할 수 있고, 클릭시 “/signin” 페이지로 이동합니다.
-- [x] 이메일 input에서 focus out 일 때, 값이 없을 경우 alert으로 “이메일을 입력해주세요.” 메세지를 보입니다.
-- [x] 이메일 input에서 focus out 일 때, 값이 있고, 이메일 형식에 맞지 않을 경우 alert으로 “올바른 이메일 주소가 아닙니다.” 메세지를 보입니다.
-- [x] 이메일 input에서 focus out 일 때, input 값이 test@codeit.com 일 경우, alert으로 “이미 사용 중인 아이디입니다.” 메세지를 보입니다.
-- [x] 비밀번호 input에서 focus out 일 때, 값이 없거나 문자열만 있거나 숫자만 있는 경우, alert으로 “비밀번호는 영문, 숫자 조합 8자 이상 입력해 주세요.” 메세지를 보입니다.
-- [x] 회원가입을 실행할 경우, 문제가 있는 경우 문제가 있는 부분을 alert 메세지로 알립니다.
-- [x] 이외의 유효한 회원가입 시도의 경우, “/my-link”로 이동합니다.
-- [x] 회원가입 버튼 클릭 또는 Enter키 입력으로 회원가입 실행돼야 합니다.
-
-#### 모바일 크기
-
-- [x] 375px 보다 작은 크기의 기기는 고려하지 않습니다.
-- [x] 좌우 여백 32px 제외하고 내부 요소들이 너비를 모두 차지합니다.
-- [x] 내부 요소들의 너비는 기기의 너비가 커질수록 커지지만 400px을 넘지 않습니다.
-
-### 선택 요구사항
-
-#### 전체
-
-- [x] 비밀번호 input 요소에 비밀번호를 확인할 수 있는 아이콘을 추가합니다.
-- [x] 비밀번호를 확인할 수 있는 아이콘 클릭시 비밀번호의 문자열이 보이기도 하고, 가려지기도 합니다.
+## Week 4
diff --git a/api/common.js b/api/common.js
new file mode 100644
index 0000000..6341152
--- /dev/null
+++ b/api/common.js
@@ -0,0 +1 @@
+export const BASE_URL = "https://bootcamp-api.codeit.kr";
diff --git a/api/folder.api.js b/api/folder.api.js
new file mode 100644
index 0000000..e6af84f
--- /dev/null
+++ b/api/folder.api.js
@@ -0,0 +1,8 @@
+import { BASE_URL } from "./common.js";
+
+const folderUrl = BASE_URL + "/api/sample/folder";
+export async function fetchFolderData() {
+ const response = await fetch(folderUrl);
+ const { data } = await response.json();
+ return data;
+}
diff --git a/api/user.api.js b/api/user.api.js
new file mode 100644
index 0000000..f41c7eb
--- /dev/null
+++ b/api/user.api.js
@@ -0,0 +1,9 @@
+import { BASE_URL } from "./common.js";
+
+const userUrl = BASE_URL + "/api/sample/user";
+
+export async function fetchUserData() {
+ const response = await fetch(userUrl);
+ const { data } = await response.json();
+ return data;
+}
diff --git a/components/card/card-component.css b/components/card/card-component.css
new file mode 100644
index 0000000..a147b30
--- /dev/null
+++ b/components/card/card-component.css
@@ -0,0 +1,99 @@
+@import url("/static/css/global_style.css");
+
+.card-container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ filter: drop-shadow(0rem 0.5rem 2.5rem rgba(0, 0, 0, 0.08));
+ border-radius: 2.5rem;
+ overflow: hidden;
+ width: 34rem;
+ height: 33.4rem;
+ background-color: var(--linkbrary-white);
+}
+
+.card-container:hover .card-image {
+ transform: scale(1.2);
+}
+
+.card-container:hover .card-info {
+ background-color: var(--library-white-smoke);
+}
+
+.card-image {
+ height: 20rem;
+ object-fit: cover;
+ overflow: hidden;
+ display: block;
+}
+
+.star-icon {
+ position: absolute;
+ top: 1.6rem;
+ right: 1.6rem;
+ z-index: 1;
+}
+
+.card-info {
+ padding: 1.5rem 2rem;
+ position: relative;
+ z-index: 1;
+ height: 13.4rem;
+}
+
+.card-info-head {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+}
+
+.card-update-time {
+ font-weight: 400;
+ font-size: 1.3rem;
+ line-height: 1.6rem;
+ color: #666666;
+}
+
+.kebab-icon {
+ width: 2.1rem;
+ height: 1.7rem;
+}
+
+.card-description {
+ font-weight: 500;
+ font-size: 1.6rem;
+ line-height: 2.4rem;
+ margin: 1rem auto;
+
+ text-overflow: ellipsis;
+ overflow: hidden;
+ word-break: break-word;
+
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.card-date {
+ font-weight: 400;
+ font-size: 1.5rem;
+ line-height: 1.8rem;
+
+ color: #333333;
+}
+
+@media screen and (max-width: 767px) {
+ .card-container {
+ width: 32.5rem;
+ height: 32.7rem;
+ }
+
+ .card-image {
+ width: 32.5rem;
+ }
+
+ .card-info {
+ height: 13.5rem;
+ padding: 1.5rem 2rem;
+ }
+}
diff --git a/components/card/card-list-component.css b/components/card/card-list-component.css
new file mode 100644
index 0000000..a8a3703
--- /dev/null
+++ b/components/card/card-list-component.css
@@ -0,0 +1,25 @@
+@import url("/static/css/global_style.css");
+
+.card-list-container {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ justify-items: center;
+ gap: 2rem;
+
+ margin-bottom: 10rem;
+}
+
+@media screen and (max-width: 1100px) {
+ .card-list-container {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media screen and (max-width: 767px) {
+ .card-list-container {
+ grid-template-columns: repeat(1, 1fr);
+ gap: 2.5rem;
+ margin-top: 1.2rem;
+ margin-bottom: 6rem;
+ }
+}
diff --git a/components/card/cardComponent.js b/components/card/cardComponent.js
new file mode 100644
index 0000000..3639dc7
--- /dev/null
+++ b/components/card/cardComponent.js
@@ -0,0 +1,114 @@
+import { StarComponent } from "../star/starComponent.js";
+export class CardComponent extends HTMLElement {
+ #prop = null;
+ constructor() {
+ super();
+
+ this.shadow = this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ get prop() {
+ return this.#prop;
+ }
+
+ set prop(newProp) {
+ // 타입체킹 로직 추후 추가
+ if (typeof newProp !== "object" && newProp !== null) {
+ console.warn("올바르지 않은 형식의 데이터가 들어왔습니다.");
+ return;
+ }
+ if (
+ typeof newProp.imageSrc !== "string" ||
+ typeof newProp.description !== "string" ||
+ typeof newProp.date !== "string" ||
+ typeof newProp.url !== "string"
+ ) {
+ console.warn("올바르지 않은 형식의 데이터가 들어왔습니다.");
+ return;
+ }
+ this.#prop = newProp;
+ }
+
+ calculateTimeDiff(dateString) {
+ const updatedDate = new Date(dateString);
+ const today = new Date();
+ const timeDiff = today - updatedDate;
+
+ const MINUTE = 60 * 1000;
+ const HOUR = MINUTE * 60;
+ const DAY = HOUR * 24;
+ const MONTH = DAY * 31;
+ const YEAR = DAY * 365;
+
+ const timeUnits = [
+ { value: YEAR, label: "year" },
+ { value: MONTH, label: "month" },
+ { value: DAY, label: "day" },
+ { value: HOUR, label: "hour" },
+ { value: MINUTE, label: "minute" },
+ ];
+
+ for (let i = 0; i < timeUnits.length; i++) {
+ const { value, label } = timeUnits[i];
+
+ if (timeDiff < value) {
+ continue;
+ }
+
+ const formattedTimeDiff = Math.floor(timeDiff / value);
+
+ return (
+ formattedTimeDiff +
+ " " +
+ label +
+ (formattedTimeDiff > 1 ? "s" : "") +
+ " ago"
+ );
+ }
+ }
+
+ parseDate(dateString) {
+ const date = new Date(dateString);
+ const year = date.getFullYear();
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return [year, month, day].join(". ");
+ }
+
+ get template() {
+ return `
+
+

+
+
+
+
${this.calculateTimeDiff(
+ this.prop.date
+ )}
+

+
+
${this.prop.description}
+
${this.parseDate(this.prop.date)}
+
+
+ `;
+ }
+ render() {
+ // CSS
+ const linkElem = document.createElement("link");
+ linkElem.setAttribute("rel", "stylesheet");
+ linkElem.setAttribute("href", "/components/card/card-component.css");
+ this.shadow.appendChild(linkElem);
+ const cardComponent = document.createElement("template");
+ cardComponent.innerHTML = this.template;
+ this.shadow.appendChild(cardComponent.content.cloneNode(true));
+
+ const starIcon = new StarComponent();
+ this.shadow.querySelector(".star-icon").appendChild(starIcon);
+ }
+}
+customElements.define("card-component", CardComponent);
diff --git a/components/card/cardListComponent.js b/components/card/cardListComponent.js
new file mode 100644
index 0000000..802fa00
--- /dev/null
+++ b/components/card/cardListComponent.js
@@ -0,0 +1,56 @@
+import { CardComponent } from "./cardComponent.js";
+
+class CardListComponent extends HTMLElement {
+ #prop = null;
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ get prop() {
+ return this.#prop;
+ }
+
+ set prop(newProp) {
+ // type checking 추후 구현
+ this.#prop = newProp;
+ this.renderCards();
+ }
+
+ createCard() {
+ const cardComponent = new CardComponent();
+ return cardComponent;
+ }
+
+ renderCards() {
+ this.prop.forEach((card) => {
+ const cardComponent = this.createCard();
+ cardComponent.prop = {
+ imageSrc: card.imageSource ?? "/static/imgs/default-card-img.png",
+ description: card.description,
+ date: card.createdAt,
+ url: card.url,
+ };
+ this.cardListContainer.appendChild(cardComponent);
+ });
+ }
+
+ render() {
+ const linkElem = document.createElement("link");
+ linkElem.setAttribute("rel", "stylesheet");
+ linkElem.setAttribute("href", "/components/card/card-list-component.css");
+ this.shadowRoot.appendChild(linkElem);
+
+ const cardListContainer = document.createElement("div");
+ cardListContainer.classList.add("card-list-container");
+ this.shadowRoot.appendChild(cardListContainer);
+ this.cardListContainer = cardListContainer;
+ }
+}
+
+customElements.define("card-list-component", CardListComponent);
diff --git a/components/folder-info/folder-info.css b/components/folder-info/folder-info.css
new file mode 100644
index 0000000..cba22ee
--- /dev/null
+++ b/components/folder-info/folder-info.css
@@ -0,0 +1,35 @@
+@import url("/static/css/global_style.css");
+
+.user {
+ margin: 2rem auto;
+}
+
+.user-name {
+ font-weight: 400;
+ font-size: 1.6rem;
+ line-height: 1.9rem;
+ margin: 1.2rem 0 0;
+}
+.codeit-avatar {
+ width: 6.4rem;
+ height: 6.4rem;
+}
+.page-heading {
+ margin: 0;
+}
+
+@media screen and (max-width: 767px) {
+ .user {
+ margin: 1rem auto;
+ }
+ .codeit-avatar {
+ width: 6.4rem;
+ height: 6.4rem;
+ }
+ .user-name {
+ font-weight: 400;
+ font-size: 1.6rem;
+ line-height: 1.9rem;
+ margin: 1.2rem 0 0;
+ }
+}
diff --git a/components/folder-info/folderInfo.js b/components/folder-info/folderInfo.js
new file mode 100644
index 0000000..3a03b85
--- /dev/null
+++ b/components/folder-info/folderInfo.js
@@ -0,0 +1,69 @@
+export class FolderInfo extends HTMLElement {
+ #prop = null;
+ constructor() {
+ super();
+ this.shadow = this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ get template() {
+ return `
+

+
${this.prop?.ownerName ?? "@코드잇"}
+
+ ${
+ this.prop?.folderName ?? "⭐️ 즐겨찾기"
+ }
`;
+ }
+
+ get prop() {
+ return this.#prop;
+ }
+
+ set prop(newProp) {
+ if (typeof newProp !== "object" && newProp !== null) {
+ console.warn("올바르지 않은 형식의 데이터가 들어왔습니다.");
+ return;
+ }
+ if (
+ typeof newProp.profileSrc !== "string" ||
+ typeof newProp.ownerName !== "string" ||
+ typeof newProp.folderName !== "string"
+ ) {
+ console.warn("올바르지 않은 형식의 데이터가 들어왔습니다.");
+ return;
+ }
+ this.#prop = newProp;
+ this.showFolderInfo();
+ }
+
+ showFolderInfo() {
+ const { profileSrc, ownerName, folderName } = this.prop;
+ this.profileImageElem.setAttribute("src", profileSrc);
+ this.owerNameElem.innerText = ownerName;
+ this.folderNameElem.innerText = folderName;
+ }
+
+ render() {
+ const styles = document.createElement("link");
+ styles.href = "/components/folder-info/folder-info.css";
+ styles.rel = "stylesheet";
+
+ this.shadow.appendChild(styles);
+ this.shadow.innerHTML += this.template;
+
+ this.profileImageElem = this.shadow.querySelector(".codeit-avatar");
+ this.owerNameElem = this.shadow.querySelector(".user-name");
+ this.folderNameElem = this.shadow.querySelector(".page-heading");
+ }
+}
+
+customElements.define("folder-info-component", FolderInfo);
diff --git a/components/footer/footer-component.css b/components/footer/footer-component.css
new file mode 100644
index 0000000..8ec4c7d
--- /dev/null
+++ b/components/footer/footer-component.css
@@ -0,0 +1,62 @@
+@import url("/static/css/global_style.css");
+
+footer {
+ background-color: var(--linkbrary-black);
+ font-family: "Arial";
+ font-weight: 400;
+ font-size: 1.6rem;
+}
+
+.footer-wrap {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 192rem;
+ margin: 0 auto;
+ padding: 3.2rem 3.2rem 10.4rem;
+}
+
+.copyright {
+ width: 10.5rem;
+ height: 1.8rem;
+ color: #676767;
+ margin: 0;
+}
+.privacy-policy-faq {
+ display: flex;
+ justify-content: space-between;
+ gap: 30;
+}
+
+.privacy-policy-faq a {
+ color: #cfcfcf;
+}
+
+.privacy-policy-faq a:first-child {
+ margin-right: 3rem;
+}
+
+footer img {
+ width: 1.8rem;
+ height: 1.8rem;
+ margin-right: 1.3rem;
+}
+
+@media screen and (max-width: 767px) {
+ .footer-wrap {
+ display: grid;
+ padding: 3.2rem;
+ align-items: start;
+ grid-template-rows: repeat(2, 1fr);
+ grid-template-columns: repeat(2, 1fr);
+ gap: 6rem 0;
+ }
+
+ .copyright {
+ order: 1;
+ }
+
+ .footer-imgs {
+ justify-self: end;
+ }
+}
diff --git a/components/footer/footerComponent.js b/components/footer/footerComponent.js
new file mode 100644
index 0000000..bf4ada0
--- /dev/null
+++ b/components/footer/footerComponent.js
@@ -0,0 +1,105 @@
+class FooterComponent extends HTMLElement {
+ constructor() {
+ super();
+
+ this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ createLinkElement() {
+ const linkElem = document.createElement("link");
+ linkElem.setAttribute("rel", "stylesheet");
+ linkElem.setAttribute("href", "/components/footer/footer-component.css");
+ return linkElem;
+ }
+
+ createCopyRightElement() {
+ const copyRight = document.createElement("p");
+ copyRight.textContent = "©codeit - 2023";
+ copyRight.classList.add("copyright");
+ return copyRight;
+ }
+
+ createPrivacyPolicyFaqElement() {
+ const privacyPolicyFaq = document.createElement("div");
+ privacyPolicyFaq.classList.add("privacy-policy-faq");
+
+ const privacyPolicy = document.createElement("a");
+ privacyPolicy.classList.add("privacy-policy");
+ privacyPolicy.textContent = "Privacy Policy";
+ privacyPolicy.href = "./privacy";
+
+ const faq = document.createElement("a");
+ faq.classList.add("faq");
+ faq.textContent = "FAQ";
+ faq.href = "./faq";
+
+ privacyPolicyFaq.appendChild(privacyPolicy);
+ privacyPolicyFaq.appendChild(faq);
+
+ return privacyPolicyFaq;
+ }
+
+ createFooterImgsElement() {
+ const footerImgs = document.createElement("div");
+ footerImgs.classList.add("footer-imgs");
+ const socialMediaLinks = [
+ {
+ href: "https://ko-kr.facebook.com/",
+ alt: "facebook",
+ src: "/static/imgs/facebook.svg",
+ },
+ {
+ href: "https://twitter.com/?lang=ko",
+ alt: "twitter",
+ src: "/static/imgs/twitter.svg",
+ },
+ {
+ href: "https://www.youtube.com/?gl=KR",
+ alt: "youtube",
+ src: "/static/imgs/youtube.svg",
+ },
+ {
+ href: "https://www.instagram.com/",
+ alt: "insta",
+ src: "/static/imgs/insta.svg",
+ },
+ ];
+
+ for (let i = 0; i < socialMediaLinks.length; i++) {
+ const socialMediaLink = document.createElement("a");
+ socialMediaLink.href = socialMediaLinks[i].href;
+
+ const socialMediaImg = document.createElement("img");
+ socialMediaImg.alt = socialMediaLinks[i].alt;
+ socialMediaImg.src = socialMediaLinks[i].src;
+ socialMediaLink.appendChild(socialMediaImg);
+ footerImgs.appendChild(socialMediaLink);
+ }
+ return footerImgs;
+ }
+
+ createFooterWrapElement() {
+ const footerWrap = document.createElement("div");
+ footerWrap.classList.add("footer-wrap");
+
+ footerWrap.appendChild(this.createCopyRightElement());
+ footerWrap.appendChild(this.createPrivacyPolicyFaqElement());
+ footerWrap.appendChild(this.createFooterImgsElement());
+
+ return footerWrap;
+ }
+
+ render() {
+ const footerContainer = document.createElement("footer");
+ footerContainer.appendChild(this.createLinkElement());
+ footerContainer.appendChild(this.createFooterWrapElement());
+
+ this.shadowRoot.appendChild(footerContainer);
+ }
+}
+
+customElements.define("footer-component", FooterComponent);
diff --git a/components/gnb/gnbComponent.js b/components/gnb/gnbComponent.js
new file mode 100644
index 0000000..e11d6f1
--- /dev/null
+++ b/components/gnb/gnbComponent.js
@@ -0,0 +1,62 @@
+class GnbComponent extends HTMLElement {
+ #prop = null;
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ get prop() {
+ return this.#prop;
+ }
+
+ set prop(newProp) {
+ this.#prop = newProp;
+ this.gnbContainer.innerHTML = this.template;
+ }
+
+ get loginContent() {
+ return this.prop ? this.loggedInText : this.loginButton;
+ }
+
+ get loginButton() {
+ return `로그인`;
+ }
+
+ get loggedInText() {
+ return `
+
+

+
${this.prop.email}
+
+ `;
+ }
+
+ get template() {
+ return `
+
+
+
+ ${this.loginContent}
+ `;
+ }
+
+ render() {
+ const linkElem = document.createElement("link");
+ linkElem.setAttribute("rel", "stylesheet");
+ linkElem.setAttribute("href", "/components/gnb/gnt-component.css");
+ this.shadowRoot.appendChild(linkElem);
+
+ const gnbContainer = document.createElement("nav");
+ gnbContainer.classList.add("gnb-container");
+ this.gnbContainer = gnbContainer;
+
+ gnbContainer.innerHTML = this.template;
+ this.shadowRoot.appendChild(gnbContainer);
+ }
+}
+customElements.define("gnb-header", GnbComponent);
diff --git a/components/gnb/gnt-component.css b/components/gnb/gnt-component.css
new file mode 100644
index 0000000..6e52094
--- /dev/null
+++ b/components/gnb/gnt-component.css
@@ -0,0 +1,100 @@
+@import url("/static/css/global_style.css");
+
+nav {
+ background-color: var(--library-white-smoke);
+ justify-content: space-between;
+ display: flex;
+ max-width: 192rem;
+ margin: 0 auto;
+}
+
+main {
+ height: auto;
+ overflow: hidden;
+}
+
+nav {
+ align-items: center;
+ height: 9.4rem;
+ padding: 0 20rem;
+}
+
+nav .logo {
+ width: 13.3rem;
+ height: 2.4rem;
+}
+
+nav .login {
+ width: 12.8rem;
+ height: 5.3rem;
+ color: #f5f5f5;
+ background: linear-gradient(
+ 90.99deg,
+ var(--linkbrary-primary) 0.12%,
+ #6ae3fe 101.84%
+ );
+ border: 0;
+ border-radius: 0.8rem;
+ padding: 1.6rem 2rem;
+ font-weight: 600;
+ font-size: 1.8rem;
+}
+
+nav .profile-icon {
+ width: 2.8rem;
+ height: 2.8rem;
+ margin-right: 1rem;
+}
+
+.user-profile {
+ display: flex;
+ align-items: center;
+}
+
+.user-email {
+ display: inline-block;
+ vertical-align: middle;
+ margin: 0;
+ color: var(--library-dark-slate-gray);
+ font-size: 1.4rem;
+ line-height: 1.7rem;
+}
+@media screen and (max-width: 1199px) {
+ nav {
+ padding: 0;
+ gap: 53.8rem;
+ justify-content: center;
+ }
+}
+
+@media screen and (max-width: 863px) {
+ nav {
+ padding: 0 3.2rem;
+ gap: 0;
+ justify-content: space-between;
+ }
+}
+
+@media screen and (max-width: 767px) {
+ nav {
+ gap: 0;
+ justify-content: space-between;
+ }
+
+ nav .login {
+ width: 8rem;
+ height: 3.7rem;
+ padding: 1rem 1.6rem;
+ font-size: 1.4rem;
+ }
+ nav .add-link {
+ width: 20rem;
+ height: 3.7rem;
+ padding: 1rem 1.6rem;
+ font-size: 1.4rem;
+ }
+
+ .user-email {
+ display: none;
+ }
+}
diff --git a/components/searchBar/search-bar-component.css b/components/searchBar/search-bar-component.css
new file mode 100644
index 0000000..433c45f
--- /dev/null
+++ b/components/searchBar/search-bar-component.css
@@ -0,0 +1,30 @@
+@import url("/static/css/global_style.css");
+
+.search-wrap {
+ position: relative;
+}
+
+.search-bar-input {
+ max-width: 106rem;
+ width: 100%;
+ height: 5.4rem;
+ padding: 1.5rem 0rem 1.5rem 4.2rem;
+ margin: 4rem auto;
+ background: #f5f5f5;
+ border: none;
+ border-radius: 1rem;
+ outline: none;
+}
+
+.search-lens-icon {
+ position: absolute;
+ top: 50%;
+ transform: translate(0%, -50%);
+ left: 1.6rem;
+}
+
+@media screen and (max-width: 767px) {
+ .search-bar-input {
+ margin: 20px auto;
+ }
+}
diff --git a/components/searchBar/searchBarComponent.js b/components/searchBar/searchBarComponent.js
new file mode 100644
index 0000000..1fbfdc1
--- /dev/null
+++ b/components/searchBar/searchBarComponent.js
@@ -0,0 +1,57 @@
+class SearchBarComponent extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ createLinkElem() {
+ const linkElem = document.createElement("link");
+ linkElem.setAttribute("rel", "stylesheet");
+ linkElem.setAttribute(
+ "href",
+ "/components/searchBar/search-bar-component.css"
+ );
+ return linkElem;
+ }
+
+ createSearchBarContainer() {
+ const searchBarContainer = document.createElement("div");
+ searchBarContainer.classList.add("search-wrap");
+ const logoImage = this.createLogoImage();
+ const searchBarInput = this.createSearchBarInput();
+
+ searchBarContainer.appendChild(logoImage);
+ searchBarContainer.appendChild(searchBarInput);
+
+ return searchBarContainer;
+ }
+
+ createLogoImage() {
+ const logoImage = document.createElement("img");
+ logoImage.classList.add("search-lens-icon");
+ logoImage.alt = "search-lens-icon";
+ logoImage.src = "/static/imgs/search-bar-lens-icon.svg";
+ return logoImage;
+ }
+
+ createSearchBarInput() {
+ const searchBarInput = document.createElement("input");
+ searchBarInput.classList.add("search-bar-input");
+ searchBarInput.placeholder = "원하는 링크를 검색해 보세요";
+ return searchBarInput;
+ }
+
+ render() {
+ const linkElem = this.createLinkElem();
+ const searchBarContainer = this.createSearchBarContainer();
+
+ this.shadowRoot.appendChild(linkElem);
+ this.shadowRoot.appendChild(searchBarContainer);
+ }
+}
+
+customElements.define("search-bar", SearchBarComponent);
diff --git a/components/star/starComponent.js b/components/star/starComponent.js
new file mode 100644
index 0000000..57517fe
--- /dev/null
+++ b/components/star/starComponent.js
@@ -0,0 +1,88 @@
+export class StarComponent extends HTMLElement {
+ #prop;
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ }
+
+ static get observedAttributes() {
+ return ["is_starred"];
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (name === "is_starred" && oldValue !== newValue) {
+ this.prop = newValue === "true";
+ this.renderStarIcon();
+ }
+ }
+
+ get prop() {
+ return this.#prop;
+ }
+
+ set prop(newProp) {
+ //추후 즐겨찾기(=starred 여부)에 대한 데이터 주고받기 할 경우 추가
+ if (typeof newProp !== "boolean") {
+ console.warn("옳바르지 않은 형식의 데이터가 들어왔습니다.");
+ return;
+ }
+ this.#prop = newProp;
+ this.setAttribute("is_starred", this.prop);
+ }
+
+ renderStarIcon() {
+ const pathColor = this.shadowRoot.querySelector("path");
+ const fillOpacity = this.prop ? "1" : "0.2";
+ const fillColor = this.prop
+ ? "var(--linkbrary-primary)"
+ : "var(--linkbrary-black)";
+
+ pathColor.setAttribute("fill", fillColor);
+ pathColor.setAttribute("fill-opacity", fillOpacity);
+ }
+
+ get template() {
+ return `
+
+ `;
+ }
+
+ toggleStarredStatus(event) {
+ event.stopPropagation();
+ this.prop = !this.prop;
+ this.setAttribute("is_starred", this.prop);
+ }
+
+ render() {
+ // CSS
+ const linkElem = document.createElement("link");
+ linkElem.setAttribute("rel", "stylesheet");
+ linkElem.setAttribute("href", "/static/css/global_style.css");
+ this.shadowRoot.appendChild(linkElem);
+
+ const starIcon = document.createElement("template");
+ starIcon.innerHTML = this.template;
+ this.shadowRoot.appendChild(starIcon.content.cloneNode(true));
+
+ this.setAttribute("is_starred", this.prop);
+ this.addEventListener("click", this.toggleStarredStatus.bind(this));
+ }
+}
+customElements.define("star-icon", StarComponent);
diff --git a/forgot-password/index.html b/forgot-password/index.html
index b138f08..873120b 100644
--- a/forgot-password/index.html
+++ b/forgot-password/index.html
@@ -1,15 +1,11 @@
-
-
-
-
-
- Document
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+ Linkbrary
+
+
+
+