Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Deploy to GitHub Pages

on:
push: # push trigger
branches:
- main
- easy

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: latest

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm run build

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./dist"

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
145 changes: 109 additions & 36 deletions e2e/e2e.spec.js

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ addEventListener("fetch", function (event) {

// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
if (
event.request.cache === "only-if-cached" &&
event.request.mode !== "same-origin"
) {
return;
}

Expand Down Expand Up @@ -216,7 +219,9 @@ async function getResponse(event, client, requestId) {
const acceptHeader = headers.get("accept");
if (acceptHeader) {
const values = acceptHeader.split(",").map((value) => value.trim());
const filteredValues = values.filter((value) => value !== "msw/passthrough");
const filteredValues = values.filter(
(value) => value !== "msw/passthrough",
);

if (filteredValues.length > 0) {
headers.set("accept", filteredValues.join(", "));
Expand Down Expand Up @@ -286,7 +291,10 @@ function sendToClient(client, message, transferrables = []) {
resolve(event.data);
};

client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
]);
});
}

Expand Down
8 changes: 7 additions & 1 deletion src/api/productApi.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// 상품 목록 조회
export async function getProducts(params = {}) {
const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params;
const {
limit = 20,
search = "",
category1 = "",
category2 = "",
sort = "price_asc",
} = params;
const page = params.current ?? params.page ?? 1;

const searchParams = new URLSearchParams({
Expand Down
14 changes: 11 additions & 3 deletions src/lib/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export class Router {
document.addEventListener("click", (e) => {
if (e.target.closest("[data-link]")) {
e.preventDefault();
const url = e.target.getAttribute("href") || e.target.closest("[data-link]").getAttribute("href");
const url =
e.target.getAttribute("href") ||
e.target.closest("[data-link]").getAttribute("href");
if (url) {
this.push(url);
}
Expand Down Expand Up @@ -111,7 +113,9 @@ export class Router {
push(url) {
try {
// baseUrl이 없으면 자동으로 붙여줌
let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url);
let fullUrl = url.startsWith(this.#baseUrl)
? url
: this.#baseUrl + (url.startsWith("/") ? url : "/" + url);

const prevFullUrl = `${window.location.pathname}${window.location.search}`;

Expand Down Expand Up @@ -170,7 +174,11 @@ export class Router {

// 빈 값들 제거
Object.keys(updatedQuery).forEach((key) => {
if (updatedQuery[key] === null || updatedQuery[key] === undefined || updatedQuery[key] === "") {
if (
updatedQuery[key] === null ||
updatedQuery[key] === undefined ||
updatedQuery[key] === ""
) {
delete updatedQuery[key];
}
});
Expand Down
88 changes: 88 additions & 0 deletions src/lib/attributeUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const PROPERTY_ONLY_ATTRIBUTES = new Set(["checked", "selected"]);

export function setAttribute(element, key, value) {
if (key === "className") {
element.className = value;
element.setAttribute("class", value);
return;
}

if (key.startsWith("data-")) {
element.setAttribute(key, value);
return;
}

if (typeof value === "boolean") {
if (PROPERTY_ONLY_ATTRIBUTES.has(key)) {
element[key] = value;
} else {
element[key] = value;
if (value) {
element.setAttribute(key, "");
} else {
element.removeAttribute(key);
}
}
return;
}

element.setAttribute(key, value);
if (key in element) {
element[key] = value;
}
}

export function removeAttribute(element, key) {
if (key === "className") {
element.className = "";
element.removeAttribute("class");
return;
}

if (key in element) {
element[key] = "";
}
element.removeAttribute(key);
}

export function updateAttributes(element, newProps, oldProps) {
const allProps = new Set([
...(newProps ? Object.keys(newProps) : []),
...(oldProps ? Object.keys(oldProps) : []),
]);

allProps.forEach((key) => {
if (key.startsWith("on")) return;

const hasNewValue = newProps && key in newProps;
const hasOldValue = oldProps && key in oldProps;
const newValue = hasNewValue ? newProps[key] : undefined;
const oldValue = hasOldValue ? oldProps[key] : undefined;

if (!hasNewValue && !hasOldValue) return;
if (hasNewValue && hasOldValue && newValue === oldValue) return;

if (!hasNewValue || newValue === null || newValue === undefined) {
if (hasOldValue) {
removeAttribute(element, key);
}
return;
}

setAttribute(element, key, newValue);
});
}

export function extractEventHandlers(props) {
const eventHandlers = [];
if (!props) return eventHandlers;

Object.keys(props).forEach((key) => {
if (key.startsWith("on") && typeof props[key] === "function") {
const eventType = key.slice(2).toLowerCase();
eventHandlers.push({ eventType, handler: props[key] });
}
});

return eventHandlers;
}
76 changes: 73 additions & 3 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,75 @@
import { addEvent } from "./eventManager";
import { updateAttributes, extractEventHandlers } from "./attributeUtils.js";

export function createElement(vNode) {}
export const elementEventHandlers = new WeakMap();

function updateAttributes($el, props) {}
export function getElementEventHandlers(element) {
return elementEventHandlers.get(element) || [];
}

function createTextNode(vNode) {
if (
vNode === null ||
vNode === undefined ||
vNode === false ||
vNode === true
) {
return document.createTextNode("");
}

if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(String(vNode));
}

return null;
}

function createFragmentFromArray(vNodeArray) {
const fragment = document.createDocumentFragment();
vNodeArray.forEach((child) => {
const element = createElement(child);
if (element) {
fragment.appendChild(element);
}
});
return fragment;
}

function createElementFromVNode(vNode) {
if (typeof vNode.type === "function") {
throw new Error("컴포넌트는 정규화 후 createElement로 생성해야 합니다.");
}

const element = document.createElement(vNode.type);

const eventHandlers = extractEventHandlers(vNode.props);
if (eventHandlers.length > 0) {
elementEventHandlers.set(element, eventHandlers);
}

updateAttributes(element, vNode.props, null);

const children = vNode.children || [];
children.forEach((child) => {
const childElement = createElement(child);
if (childElement) {
element.appendChild(childElement);
}
});

return element;
}

export function createElement(vNode) {
const textNode = createTextNode(vNode);
if (textNode) return textNode;

if (Array.isArray(vNode)) {
return createFragmentFromArray(vNode);
}

if (vNode && typeof vNode === "object" && vNode.type) {
return createElementFromVNode(vNode);
}

return document.createTextNode("");
}
15 changes: 14 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
export function createVNode(type, props, ...children) {
return {};
return {
type: type,
props: props,
children: filterTruthy(children.flat(Infinity)),
};
}

function filterTruthy(children) {
return children.filter((child) => {
if (child === false) return false;
if (child === null) return false;
if (child === undefined) return false;
return true;
});
}
72 changes: 69 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,71 @@
export function setupEventListeners(root) {}
const eventHandlers = new WeakMap();

export function addEvent(element, eventType, handler) {}
function getElementHandlers(element) {
if (!eventHandlers.has(element)) {
eventHandlers.set(element, new Map());
}
return eventHandlers.get(element);
}

export function removeEvent(element, eventType, handler) {}
export function addEvent(element, eventType, handler) {
const handlers = getElementHandlers(element);
if (!handlers.has(eventType)) {
handlers.set(eventType, new Set());
}
handlers.get(eventType).add(handler);
}

export function removeEvent(element, eventType, handler) {
const handlers = getElementHandlers(element);
if (handlers.has(eventType)) {
handlers.get(eventType).delete(handler);
}
}

const rootEventListeners = new WeakMap();

export function setupEventListeners(root) {
let registeredHandlers = rootEventListeners.get(root);
if (!registeredHandlers) {
registeredHandlers = new Map();
rootEventListeners.set(root, registeredHandlers);
}

const createDelegatedHandler = (eventType) => {
return (event) => {
if (event.eventPhase === Event.BUBBLING_PHASE && event.cancelBubble) {
return;
}

let target = event.target;

while (target && target !== root && target !== document) {
const handlers = eventHandlers.get(target);
if (handlers && handlers.has(eventType)) {
const handlerSet = handlers.get(eventType);
handlerSet.forEach((handler) => {
handler(event);
});
break;
}
target = target.parentElement;
}
};
};

const collectEventTypes = (element) => {
const handlers = eventHandlers.get(element);
if (handlers) {
handlers.forEach((_, eventType) => {
if (!registeredHandlers.has(eventType)) {
const delegatedHandler = createDelegatedHandler(eventType);
registeredHandlers.set(eventType, delegatedHandler);
root.addEventListener(eventType, delegatedHandler, false);
}
});
}
Array.from(element.children).forEach(collectEventTypes);
};

collectEventTypes(root);
}
Loading
Loading