Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
69d7341
빈 커밋
0miiii Jul 7, 2025
f1fa717
Update issue templates
0miiii Jul 7, 2025
62346ed
Update issue templates
0miiii Jul 7, 2025
12816dc
chore: lint fix
0miiii Jul 9, 2025
bdcb6cb
feat: router 추가
0miiii Jul 9, 2025
b1e7238
feat: routes 추가
0miiii Jul 9, 2025
a8e5247
feat: templates 추가
0miiii Jul 9, 2025
c74b5c9
feat: store 추가
0miiii Jul 9, 2025
2ac8378
feat: main store 추가
0miiii Jul 9, 2025
c75a439
feat: 진입점에 router 설정
0miiii Jul 9, 2025
ad8bfd3
feat: 컴포넌트 추가
0miiii Jul 9, 2025
9e37065
feat: 정렬 필터 상태 추가
0miiii Jul 9, 2025
ff6bc8c
feat: 검색 상태 추가
0miiii Jul 9, 2025
9777306
chore: 임시 저장
0miiii Jul 9, 2025
73385e1
Merge branch 'main' of https://github.com/0miiii/front_6th_chapter1-1
0miiii Jul 9, 2025
5ffefaf
feat: search2 상태 추가
0miiii Jul 9, 2025
6cd78bb
feat: bread crumb 이벤트 추가
0miiii Jul 9, 2025
d016e0c
feat: category2 버튼 컬러 설정
0miiii Jul 9, 2025
ae35e2b
feat: easy / hard 를 구분하여 테스트를 실행할 수 있도록 함
JunilHwang Jul 10, 2025
c2657bc
Merge branch 'main' of https://github.com/hanghae-plus/front_6th_chap…
jym0421 Jul 10, 2025
a2a2b3c
Merge https://github.com/0miiii/front_6th_chapter1-1
jym0421 Jul 10, 2025
769afef
feat: 장바구니 컴포넌트 추가
jym0421 Jul 10, 2025
8969137
feat: detail page 추가
jym0421 Jul 10, 2025
8790a7b
feat: component core 구현
jym0421 Jul 10, 2025
5052bcd
feat: header 구조 변경
jym0421 Jul 10, 2025
3f3bc94
feat: legacy 분류
jym0421 Jul 11, 2025
9e58dc9
feat: url search params store 추가
jym0421 Jul 11, 2025
69f6d58
feat: core component 수정
jym0421 Jul 11, 2025
f4518cb
feat: 컴포넌트 클래스 형태로 개선
jym0421 Jul 11, 2025
c9cc858
feat: 개수 변경시 목록 업데이트
jym0421 Jul 11, 2025
f5f6e9f
feat: 검색필터 이벤트 추가
jym0421 Jul 11, 2025
9f5d1dc
feat: breadcrumbs 상태 추가
jym0421 Jul 11, 2025
e62095f
feat: 레거시 정리
jym0421 Jul 11, 2025
8160daf
Merge pull request #2 from ciel-youngmin/main
0miiii Jul 11, 2025
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
17 changes: 17 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

### 개요

### 구현내용

### 요구사항
- [ ] TODO
- [ ] TODO
- [ ] TODO
47 changes: 43 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,45 @@ on:
workflow_dispatch:

jobs:
unit:
hard-basic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: |
pnpm install
pnpm run test:hard:basic
hard-advanced:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: |
pnpm install
npx playwright install --with-deps
pnpm run test:hard:advanced
easy-basic:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -27,8 +65,9 @@ jobs:
- name: Install dependencies
run: |
pnpm install
pnpm run test
e2e:
npx playwright install --with-deps
pnpm run test:easy:basic
easy-advanced:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
Expand All @@ -47,4 +86,4 @@ jobs:
run: |
pnpm install
npx playwright install --with-deps
pnpm run test:e2e
pnpm run test:easy:advanced
305 changes: 305 additions & 0 deletions e2e/e2e-easy.advanced.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import { expect, test } from "@playwright/test";

// 테스트 설정
test.describe.configure({ mode: "serial" });

// 헬퍼 함수들
class E2EHelpers {
constructor(page) {
this.page = page;
}

// 페이지 로딩 대기
async waitForPageLoad() {
await this.page.waitForSelector('[data-testid="products-grid"], #products-grid', { timeout: 10000 });
await this.page.waitForFunction(() => {
const text = document.body.textContent;
return text.includes("총") && text.includes("개");
});
}

// 상품을 장바구니에 추가
async addProductToCart(productName) {
await this.page.click(
`text=${productName} >> xpath=ancestor::*[contains(@class, 'product-card')] >> .add-to-cart-btn`,
);
await this.page.waitForSelector("text=장바구니에 추가되었습니다", { timeout: 5000 });
}

// 장바구니 모달 열기
async openCartModal() {
await this.page.click("#cart-icon-btn");
await this.page.waitForSelector(".cart-modal-overlay", { timeout: 5000 });
}
}

test.describe("E2E: 쇼핑몰 전체 사용자 시나리오", () => {
test.beforeEach(async ({ page }) => {
// 로컬 스토리지 초기화
await page.goto("/");
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
});

test.describe("1. 검색 및 필터링 기능이 URL과 연동된다.", () => {
test("검색어 입력 후 Enter 키로 검색하고 URL이 업데이트된다", async ({ page }) => {
const helpers = new E2EHelpers(page);
await helpers.waitForPageLoad();

// 검색어 입력
await page.fill("#search-input", "젤리");
await page.press("#search-input", "Enter");

// URL 업데이트 확인
await expect(page).toHaveURL(/search=%EC%A0%A4%EB%A6%AC/);

// 검색 결과 확인
await expect(page.locator("text=3개")).toBeVisible();

// 검색어가 검색창에 유지되는지 확인
await expect(page.locator("#search-input")).toHaveValue("젤리");

// 검색어 입력
await page.fill("#search-input", "아이패드");
await page.press("#search-input", "Enter");

// URL 업데이트 확인
await expect(page).toHaveURL(/search=%EC%95%84%EC%9D%B4%ED%8C%A8%EB%93%9C/);

// 검색 결과 확인
await expect(page.locator("text=21개")).toBeVisible();

// 새로고침을 해도 유지 되는지 확인
await page.reload();
await helpers.waitForPageLoad();
await expect(page.locator("text=21개")).toBeVisible();
});

test("정렬 옵션 변경 시 URL이 업데이트된다", async ({ page }) => {
const helpers = new E2EHelpers(page);
await helpers.waitForPageLoad();

// 가격 높은순으로 정렬
await page.selectOption("#sort-select", "price_desc");

// 첫 번째 상품 이 가격 높은 순으로 정렬되었는지 확인
await expect(page.locator(".product-card").first()).toMatchAriaSnapshot(`
- img "ASUS ROG Flow Z13 GZ302EA-RU110W 64GB, 1TB"
- heading "ASUS ROG Flow Z13 GZ302EA-RU110W 64GB, 1TB" [level=3]
- paragraph: ASUS
- paragraph: 3,749,000원
- button "장바구니 담기"
`);

await page.selectOption("#sort-select", "name_asc");
await expect(page.locator(".product-card").nth(1)).toMatchAriaSnapshot(`
- img "[매일출발]유로블루플러스 차량용 요소수 국내산 Adblue 호스포함"
- heading "[매일출발]유로블루플러스 차량용 요소수 국내산 Adblue 호스포함" [level=3]
- paragraph: 유로블루플러스
- paragraph: 8,700원
- button "장바구니 담기"
`);

await page.selectOption("#sort-select", "name_desc");
await expect(page.locator(".product-card").nth(1)).toMatchAriaSnapshot(`
- img "P&G 다우니 울트라 섬유유연제 에이프릴 프레쉬, 5.03L, 1개"
- heading "P&G 다우니 울트라 섬유유연제 에이프릴 프레쉬, 5.03L, 1개" [level=3]
- paragraph: 다우니
- paragraph: 16,610원
- button "장바구니 담기"
`);

await page.reload();
await helpers.waitForPageLoad();
await expect(page.locator(".product-card").nth(1)).toMatchAriaSnapshot(`
- img "P&G 다우니 울트라 섬유유연제 에이프릴 프레쉬, 5.03L, 1개"
- heading "P&G 다우니 울트라 섬유유연제 에이프릴 프레쉬, 5.03L, 1개" [level=3]
- paragraph: 다우니
- paragraph: 16,610원
- button "장바구니 담기"
`);
});

test("페이지당 상품 수 변경 시 URL이 업데이트된다", async ({ page }) => {
const helpers = new E2EHelpers(page);
await helpers.waitForPageLoad();

// 10개로 변경
await page.selectOption("#limit-select", "10");
await expect(page).toHaveURL(/limit=10/);
await page.waitForFunction(() => {
return document.querySelectorAll(".product-card").length === 10;
});
await expect(page.locator(".product-card").last()).toMatchAriaSnapshot(
`- heading "탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm" [level=3]`,
);

await page.selectOption("#limit-select", "20");
await expect(page).toHaveURL(/limit=20/);
await page.waitForFunction(() => {
return document.querySelectorAll(".product-card").length === 20;
});
await expect(page.locator(".product-card").last()).toMatchAriaSnapshot(
`- heading "고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m" [level=3]`,
);

await page.selectOption("#limit-select", "50");
await expect(page).toHaveURL(/limit=50/);
await page.waitForFunction(() => {
return document.querySelectorAll(".product-card").length === 50;
});
await expect(page.locator(".product-card").last()).toMatchAriaSnapshot(
`- heading "강아지 고양이 아이스팩 파우치 여름 베개 젤리곰 M사이즈" [level=3]`,
);

await page.selectOption("#limit-select", "100");
await expect(page).toHaveURL(/limit=100/);
await page.waitForFunction(() => {
return document.querySelectorAll(".product-card").length === 100;
});
await expect(page.locator(".product-card").last()).toMatchAriaSnapshot(
`- heading "고양이 스크래쳐 숨숨집 하우스 대형 원목 스크레쳐 A type" [level=3]`,
);

await page.reload();
await helpers.waitForPageLoad();
await expect(page.locator(".product-card").last()).toMatchAriaSnapshot(
`- heading "고양이 스크래쳐 숨숨집 하우스 대형 원목 스크레쳐 A type" [level=3]`,
);
});

test("검색어와 필터 조건이 URL에서 복원된다", async ({ page }) => {
const helpers = new E2EHelpers(page);

await page.goto("/?search=젤리&sort=price_desc&limit=10");
await helpers.waitForPageLoad();

await expect(page.locator("#search-input")).toHaveValue("젤리");
await expect(page.locator("#sort-select")).toHaveValue("price_desc");
await expect(page.locator("#limit-select")).toHaveValue("10");
await expect(page.getByRole("main")).toMatchAriaSnapshot(`- text: /총 3개의 상품/`);

await page.goto("/?search=고양이&sort=name_desc&limit=50");
await helpers.waitForPageLoad();

await expect(page.locator("#search-input")).toHaveValue("고양이");
await expect(page.locator("#sort-select")).toHaveValue("name_desc");
await expect(page.locator("#limit-select")).toHaveValue("50");
await expect(page.getByRole("main")).toMatchAriaSnapshot(`- text: /총 84개의 상품/`);
});
});

test.describe("2. 상품 상세 페이지와 URL이 연동된다.", () => {
test("상품 클릭부터 관련 상품 이동까지 전체 플로우", async ({ page }) => {
await page.evaluate(() => {
window.loadFlag = true;
window.history.pushState({}, "", "/product/85067212996");
window.dispatchEvent(new Event("popstate"));
});

// 상세 페이지 로딩 확인
await expect(page.locator("text=상품 상세")).toBeVisible();

// h1 태그에 상품명 확인
await expect(
page.locator('h1:text("PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장")'),
).toBeVisible();

// 관련 상품 섹션 확인
await expect(page.locator("text=관련 상품")).toBeVisible();
const relatedProducts = page.locator(".related-product-card");
await expect(relatedProducts.first()).toBeVisible();

// 첫 번째 관련 상품 클릭
await relatedProducts.first().click();

// 다른 상품의 상세 페이지로 이동했는지 확인
await expect(page).toHaveURL("/product/86940857379");
await expect(
page.locator('h1:text("샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이")'),
).toBeVisible();

await expect(await page.evaluate(() => window.loadFlag)).toBe(true);

await page.reload();

await expect(
page.locator('h1:text("샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이")'),
).toBeVisible();

await expect(await page.evaluate(() => window.loadFlag)).toBe(undefined);
});
});

test.describe("3. SPA 네비게이션", () => {
test("브라우저 뒤로가기/앞으로가기가 올바르게 작동한다", async ({ page }) => {
const helpers = new E2EHelpers(page);
await page.evaluate(() => {
window.loadFlag = true;
});
await helpers.waitForPageLoad();

// 상품 상세 페이지로 이동
const productCard = page
.locator("text=PVC 투명 젤리 쇼핑백")
.locator('xpath=ancestor::*[contains(@class, "product-card")]');
await productCard.locator("img").click();

await expect(page).toHaveURL("/product/85067212996");
await expect(
page.locator('h1:text("PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장")'),
).toBeVisible();
await expect(page.locator("text=관련 상품")).toBeVisible();
const relatedProducts = page.locator(".related-product-card");
await relatedProducts.first().click();

await expect(page).toHaveURL("/product/86940857379");
await expect(
page.locator('h1:text("샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이")'),
).toBeVisible();

// 브라우저 뒤로가기
await page.goBack();
await expect(page).toHaveURL("/product/85067212996");
await expect(
page.locator('h1:text("PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장")'),
).toBeVisible();

// 브라우저 앞으로가기
await page.goForward();
await expect(page).toHaveURL("/product/86940857379");
await expect(
page.locator('h1:text("샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이")'),
).toBeVisible();

await page.goBack();
await page.goBack();
await expect(page).toHaveURL("/");
const firstProductCard = page.locator(".product-card").first();
await expect(firstProductCard.locator("img")).toBeVisible();

expect(await page.evaluate(() => window.loadFlag)).toBe(true);

await page.reload();
expect(
await page.evaluate(() => {
return window.loadFlag;
}),
).toBe(undefined);
});

// 404 페이지 테스트
test("존재하지 않는 페이지 접근 시 404 페이지가 표시된다", async ({ page }) => {
// 존재하지 않는 경로로 이동
await page.goto("/non-existent-page");

// 404 페이지 확인
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- img: /404 페이지를 찾을 수 없습니다/
- link "홈으로"
`);
});
});
});
Loading
Loading