Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1단계 - 맛있는 맛집 미션] - 마빈(김재영) 미션 제출합니다. #180

Merged
merged 46 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
08f7086
​feat: coauthor 설정
wo-o29 Mar 4, 2025
70294d7
​docs: 초기 기능 목록 작성
wo-o29 Mar 4, 2025
454019c
chore: 초기 설정(불필요한 파일, 패키지 제거)
wo-o29 Mar 4, 2025
40fee43
​feat: biome 설정
wo-o29 Mar 4, 2025
937c9ee
​feat: dom 생성 유틸 함수 추가
wo-o29 Mar 4, 2025
a953b15
​feat: auto import 추가
wo-o29 Mar 4, 2025
5118cfd
​feat: 헤더 컴포넌트 추가
wo-o29 Mar 4, 2025
aadaa11
​feat: 음식점 리스트 아이템 추가
wo-o29 Mar 4, 2025
7e3e62c
​feat: dropdownBox 컴포넌트 추가
wo-o29 Mar 4, 2025
133eceb
​feat: inputBox 컴포넌트 추가
wo-o29 Mar 4, 2025
3880323
​feat: button 컴포넌트 추가
wo-o29 Mar 4, 2025
10dce50
​feat: 카테고리, 거리 상수 추가
wo-o29 Mar 4, 2025
745a580
fix: 클래스 네임 중복 처리 수정​
wo-o29 Mar 4, 2025
d7d73d4
​feat: textArea 컴포넌트 추가
wo-o29 Mar 4, 2025
816194b
​feat: restaurantForm 컴포넌트 추가
wo-o29 Mar 4, 2025
a65ed6a
​feat: 에러 메세지 상수 추가
wo-o29 Mar 4, 2025
17e404b
​refactor: 상수에 빠진 부분 추가
wo-o29 Mar 5, 2025
af119ee
​fix: 드롭다운에 id를 부여하지 않았던 이슈 수정
wo-o29 Mar 5, 2025
5ececb8
fix: required 속성 디폴트 파라미터 설정
wo-o29 Mar 5, 2025
cd40659
​feat: isInRange 유틸 함수 추가
wo-o29 Mar 5, 2025
1cb26ed
feat: ​extractByKey, extractFormData 유틸 함수 추가
wo-o29 Mar 5, 2025
58abd86
feat: 음식점 추가 유효성 검사 함수 추가​
wo-o29 Mar 5, 2025
54238ce
​feat: 음식점 추가 폼 데이터 처리
wo-o29 Mar 5, 2025
85ae0f7
​feat: 음식점 추가 로직 + 모달 닫기 구현
wo-o29 Mar 5, 2025
d7cf951
​feat: 1차 구현 완료
wo-o29 Mar 5, 2025
b6549b4
​refactor: 필요없는 html 코드 제거
wo-o29 Mar 5, 2025
1806c7c
​feat: 모달 애니메이션 추가
wo-o29 Mar 5, 2025
c457d4c
​refactor: 안쓰는 코드 제거
wo-o29 Mar 5, 2025
e662711
​test: cypress viewport 설정 추가
wo-o29 Mar 5, 2025
168801d
​refactor: 음식점 추가 폼 클래스 추가
wo-o29 Mar 5, 2025
8ad030e
test: 음식점 추가 시나리오 e2e 테스트 코드 작성
wo-o29 Mar 5, 2025
dda38f3
​remove: 안쓰는 파일 삭제
wo-o29 Mar 5, 2025
9990966
test: 음식점 이름 경고창 나오는 시나리오 추가
wo-o29 Mar 6, 2025
5f90dd6
​test: 안되는 시나리오 추가(음식점 이름, 설명, 링크)
wo-o29 Mar 6, 2025
e1e5514
docs: 궁금한 사항 추가​
wo-o29 Mar 6, 2025
6b700ed
remove: 안쓰는 파일 제거
wo-o29 Mar 6, 2025
54eee08
​remove: 안쓰는 파일 제거
wo-o29 Mar 6, 2025
44d175c
refactor: 이벤트 핸들러 함수 분리
wo-o29 Mar 6, 2025
f43595a
​refactor: 불필요한 함수 제거
wo-o29 Mar 6, 2025
bcc3647
​refactor: DOM 셀렉팅 줄이기
wo-o29 Mar 6, 2025
e5a5f28
​refactor: css 파일 분리
wo-o29 Mar 6, 2025
565a45f
​feat: fragment 추가하는 로직 유틸화(auto-import 설정)
wo-o29 Mar 6, 2025
2f4b32c
​refactor: input, textarea 등 관련 앨리먼트 `createElement`로 생성(xss 방지)
wo-o29 Mar 6, 2025
9214498
docs(readme): 갱신
spoyodevelop Mar 6, 2025
0e2d783
chores: deploy url 설정
spoyodevelop Mar 6, 2025
9ec3213
fix: alert창을 stub으로 연결하고, should를 재 연결해서, 정상적인 동작로직으로 수정
spoyodevelop Mar 6, 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
11 changes: 0 additions & 11 deletions .eslintrc.json

This file was deleted.

1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
Expand Down
Empty file removed .gitkeep
Empty file.
3 changes: 0 additions & 3 deletions .prettierrc

This file was deleted.

8 changes: 8 additions & 0 deletions .vscode/setting.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.organizeImports.biome": "explicit"
}
}
136 changes: 136 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
## 프로그래밍 요구사항

- 핵심이 되는 기능이라고 생각하는 기능 플로우에 대해 E2E 테스트를 진행한다.
- 컴포넌트 단위로 구현하는 것을 고민하고 적용해본다.

## 구현할 기능 목록

- [x] 우측 상단의 추가 버튼을 클릭하면 모달이 나와서 음식점을 추가할 수 있다.

- [x] 음식점의 카테고리, 이름, 거리(도보 이동 시간), 설명, 참고 링크를 입력해서 추가할 수 있다.
- [x] 카테고리, 거리는 셀렉트 박스, 이름/설명/참고 링크는 텍스트 인풋을 사용한다.
- [x] 카테고리, 이름, 거리는 입력 필수.

- [x] 카테고리는 "한식", "중식", "일식", "아시안", "양식", "기타" 중 하나를 선택한다.
- [x] 거리는 캠퍼스로부터 도보로 걸리는 시간(분). 5, 10, 15, 20, 30 중 하나를 선택한다.

- [x] 설명, 참고 링크는 옵션. 입력하지 않아도 음식점을 추가할 수 있어야 한다.
- [x] 입력값이 잘못되었을 때 사용자에게 알려주는 방식은 자유롭게 구현한다.
- [x] 새로고침 시 이전에 추가한 새로운 음식점 정보는 초기화된다.

## 유저 플로우

1. 추가 버튼을 클릭한다.
2. 음식점 추가 폼을 작성한다.
3. 필수 항목을 입력하지 않은 경우 `추가하기` 버튼을 클릭할 수 없다.
4. 필수 항목을 입력했지만 유효성 검사에 걸리는 경우 경고창으로 알맞는 에러 메세지를 보여준다.
5. 유효성 검사를 통과한 경우 음식점을 리스트에 추가한다.

## 폼 유효성 검사

1. 카테고리: "한식", "중식", "일식", "아시안", "양식", "기타" 중 하나
2. 음식점 이름: 1글자 이상 12자 이하
3. 거리: 5, 10, 15, 20, 30(분) 중 하나
4. 설명(옵션): 300자 이하
5. 참고 링크(옵션): 300자 이하

### 에러 처리

유효성 검사에 통과하지 못한 경우 경고창으로 에러 메세지를 보여준다.

---

## 리뷰어에게 궁금한 사항들

### E2E 시나리오 테스트 파일 관리 질문

E2E 테스트에 음식점 추가 폼이 잘 동작하는 지에 대한 여러 시나리오들이 있는데 E2E 테스트이다 보니깐 테스트 하나가 내용이 길어서 한 파일에 다 넣으면 가독성이 떨어지고 유지보수 하기 힘들 것 같다는 생각이 들었습니다.

또한, 저와 페어의 의견이 갈린 지점이 있습니다. 저는 최대한 테스트를 잘게 쪼개서 어느정도 유닛 테스트를 한뒤, 통합, e2e 테스트를 하는데요.

그런데, 만약 테스트 예산이 한정이 되어있다는하에, 만약, 최종적으로 e2e에서 모든 것을 체크한다고 하면, 기존에 있었던 통합, 유닛 테스트는 필요가 없다고 생각할수 있을까요?

cypress는 e2e 테스트를 하는, 그리고 실제로의 구현 방식도 유저가 클릭하는 것을 그대로 모방하여 매크로 처럼 동작을 하는 것 같던데, 이것 툴 자체가 유닛, 통합 테스트가 번거롭게 작용하는 것도 한몫하는 것 같구요.

쵸파의 생각이 어떤지 궁금합니다. cypress로 유닛, 통합도 하시는 편인가요?

### DOM 접근 비용 관련 질문

기존 이벤트 핸들러 코드에서는 모달에 `음식점 추가 폼(DOM 앨리먼트)`이 없다면 새로 생성하고,
`음식점 추가 폼`이 있다면 DOM을 추가하지 않는 방식을 사용했었습니다.

하지만 이렇게 하면 이벤트가 발생할 때마다 `restaurant-add-form(음식점 추가 폼)`이 있는지 DOM에 접근해서 확인을 해줘야 해서 `isFirstRender`라는 상태를 생성해 모달이 열렸던 적이 있다면 폼을 생성하지 않는 방식으로 **DOM 요소에 접근하는 방식을 최소화하였는데 혹시 더 좋은 방안**이 있을까요??

- as-is(`restaurant-add-form` DOM이 있는지 체크)

```js
function handleBottomSheetToggle(event) {
const modal = document.querySelector(".modal");

if (event.target.closest(".restaurant-add-button")) {
modal.showModal();

const restaurantAddForm = document.querySelector(".restaurant-add-form");

if (!restaurantAddForm) {
const modalContainer = document.querySelector(".modal-container");
const restaurantFrom = createRestaurantForm();
modalContainer.appendChild(restaurantFrom);
}
}

if (event.target.closest(".modal-backdrop")) {
modal.close();
}
}
```

- to-be(`isFirstRender` 상태 사용)

```js
function bottomSheetController() {
let isFirstRender = false;

function handleBottomSheetToggle(event) {
const modal = document.querySelector(".modal");

if (event.target.closest(".restaurant-add-button")) {
modal.showModal();

if (!isFirstRender) {
const modalContainer = document.querySelector(".modal-container");
const restaurantFrom = createRestaurantForm();

modalContainer.appendChild(restaurantFrom);
isFirstRender = true;
}
}

if (event.target.closest(".modal-backdrop")) {
modal.close();
}
}

return { handleBottomSheetToggle };
}
```

## css 워터폴

동료랑 진행을 코드를 짜던 중에, main css에 컴포넌트의 css파일을 import하는 과정이 있었습니다.

전혀 생각을 안하고 있었는데, 하나하나 임포트를 하고 어느정도의 워터폴이 생기는 것이 보였습니다.

결국은 조금 코드가 의아할수 있지만, html에 css를 임포트 하는 방식으로 진행을 했습니다.

애초에 처음부터 이것이 의미가 있는 작업인지, 혹시 워터폴이 발생할수 있는지에 대한 궁금증이 존재합니다.

## 피그마에 있는 그대로 구현 하는것이 좋은가요?

동료랑 이야기를 하던 중에, 현재 구현 사항에서는 사실상 컴포넌트 분리가 우선시되고, 기능 동작, 구현 사항에는 '피그마에 있는 기본값 레스토랑 리스트(피양콩컴퍼니,친친)'같은 것을 구현하라고 적혀지지 않았다는 것을 발견했습니다.

조금, 고민과 이야기를 나누었는데, 저는 클라이언트 입장에서 보면 굉장히 당황해 할것이라고 생각해, 해당 기본값 리스트를 구현을 해두는것이 중요하다고 생각했지만, 동료의 입장으로는 해당 사항이 step2에 구현해도 늦지 않다라고 생각 하고 있더군요.

한편, 동료의 입장도 이해가 됩니다. step2에 정렬을 하게 되면, 기존에 보관했던 레스트랑 리스트의 포멧과 자료 구조를 바꿔야 할수 있으니까요.

혹시 비슷한 사례가 있으면, 이야기를 나누어 줄수 있으실까요? 그리고, 클라이언트와 개발자의 소통의 레벨에 따라서 유연하게 넘어가는 편일까요?
Empty file removed __tests__/.gitkeep
Empty file.
11 changes: 11 additions & 0 deletions auto-imports.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const createElement: typeof import('@/utils/dom.js')['createElement']
const createElementsFragment: typeof import('@/utils/dom.js')['createElementsFragment']
}
24 changes: 24 additions & 0 deletions biome.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 요런 것도 있었군요 👀

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

같이 페어 했던 써밋이 추천해준 linter+formatter 통합 솔루션입니다. 전반적으로 포맷팅이 매우 빠르게 동작하며, 따로 하나하나 규칙을 지정해 줄 필요 없이,

"rules": {
      "recommended": true
    }

이렇게 하면, 스마트 하게, 조금 이상한 코드 있으면 짚어 줍니다.
한번 써봤는데, 나름 좋더라구요.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignore": [".vscode/**", "package.json", "node_modules/**"]
}
}
3 changes: 3 additions & 0 deletions coauthor.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

Co-authored-by: spoyodevelop <[email protected]>
2 changes: 2 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { defineConfig } from "cypress";

export default defineConfig({
viewportWidth: 1920,
viewportHeight: 1080,
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
Expand Down
Empty file removed cypress/.gitkeep
Empty file.
151 changes: 151 additions & 0 deletions cypress/e2e/resturantForm.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ERROR_MESSAGE } from "../../src/settings/settings.js";

describe("음식점 추가가 잘 되는지 확인하는 시나리오", () => {
beforeEach(() => {
cy.visit("http://localhost:5173/");
});

it("모달 열기 닫기 테스트, 음식점 추가(카테고리, 이름, 거리 입력)", () => {
// 모달 띄우기
cy.get(".gnb__button").click();
cy.get(".modal").should("be.visible");
cy.get(".restaurant-add-form").should("exist");

// 모달 닫기
cy.get(".cancel-button").click();
cy.get(".modal").should("not.be.visible");

// 모달 띄우기
cy.get(".gnb__button").click();
cy.get(".modal").should("be.visible");
cy.get(".restaurant-add-form").should("exist");

// 폼 데이터 입력
cy.get("#category").select("중식");
cy.get("#name").type("마담밍");
cy.get("#distance").select("20");

// 폼 제출
cy.get(".restaurant-add-form").submit();
cy.get(".restaurant").should("have.length", 1);
});

it("모달 열기 테스트, 음식점 추가 2개", () => {
// 모달 띄우기
cy.get(".gnb__button").click();
cy.get(".modal").should("be.visible");
cy.get(".restaurant-add-form").should("exist");

// 폼 데이터 입력
cy.get("#category").select("양식");
cy.get("#name").type("타코집");
cy.get("#distance").select("10");

// 폼 제출
cy.get(".restaurant-add-form").submit();
cy.get(".restaurant").should("have.length", 1);

// 모달 띄우기
cy.get(".gnb__button").click();
cy.get(".modal").should("be.visible");
cy.get(".restaurant-add-form").should("exist");

// 폼 데이터 입력
cy.get("#category").select("일식");
cy.get("#name").type("잇쇼우");
cy.get("#distance").select("15");

// 폼 제출
cy.get(".restaurant-add-form").submit();
cy.get(".restaurant").should("have.length", 2);
});
});

describe("안되는 시나리오(경고창 나오는지 테스트)", () => {
beforeEach(() => {
cy.visit("http://localhost:5173/");

cy.window().then((win) => {
cy.stub(win, "alert").as("alertStub");
});
});

it("모달 열기 테스트, 음식점 추가(음식점 이름을 15자를 입력하여 경고창을 발생시킨다.)", () => {
// 모달 띄우기
cy.get(".gnb__button").click();
cy.get(".modal").should("be.visible");
cy.get(".restaurant-add-form").should("exist");

// 폼 데이터 입력(이름 12글자 초과)
cy.get("#category").select("한식");
cy.get("#name").type("대충이름이긴한식부페같은것마아앙");
cy.get("#distance").select("5");

// 경고창 확인
cy.get(".restaurant-add-form").submit();
cy.get("@alertStub").should(
"have.been.calledOnceWith",
ERROR_MESSAGE.INVALID_RESTAURANT_NAME_LENGTH
);

// 재입력(올바른 입력 테스트)
cy.get("#name").clear();
cy.get("#name").type("더휴웨딩몰");
cy.get(".restaurant-add-form").submit();
cy.get(".restaurant").should("have.length", 1);
});

it("모달 열기 테스트, 음식점 추가(음식점 설명이 300자를 초과해서 경고창을 발생시킨다.)", () => {
// 모달 띄우기
cy.get(".gnb__button").click();
cy.get(".modal").should("be.visible");
cy.get(".restaurant-add-form").should("exist");

// 폼 데이터 입력(설명 300글자 초과)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엣지 케이스 좋네요 👍 👍
300자를 미리 알려주는 UI가 있으면 좋을 것 같습니다~!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

placeHolder요! 추가 했습니다.
토스트 팝업도 추가 할 예정이긴 합니다.

cy.get("#category").select("한식");
cy.get("#name").type("안녕하세요");
cy.get("#distance").select("5");
cy.get("#description").type(
Array.from({ length: 301 }, () => "a").join("")
);

// 경고창 확인
cy.get(".restaurant-add-form").submit();
cy.get("@alertStub").should(
"have.been.calledOnceWith",
ERROR_MESSAGE.INVALID_RESTAURANT_DESCRIPTION_LENGTH
);

// 재입력(올바른 입력 테스트)
cy.get("#description").clear();
cy.get("#description").type("강추!");
cy.get(".restaurant-add-form").submit();
cy.get(".restaurant").should("have.length", 1);
});

it("모달 열기 테스트, 음식점 추가(음식점 링크가 300자를 초과해서 경고창을 발생시킨다.)", () => {
// 모달 띄우기
cy.get(".gnb__button").click();
cy.get(".modal").should("be.visible");
cy.get(".restaurant-add-form").should("exist");

// 폼 데이터 입력(링크 300글자 초과)
cy.get("#category").select("한식");
cy.get("#name").type("맛집");
cy.get("#distance").select("10");
cy.get("#link").type(Array.from({ length: 301 }, () => "a").join(""));

// 경고창 확인
cy.get(".restaurant-add-form").submit();
cy.get("@alertStub").should(
"have.been.calledOnceWith",
ERROR_MESSAGE.INVALID_RESTAURANT_LINK_LENGTH
);

// 재입력(올바른 입력 테스트)
cy.get("#link").clear();
cy.get("#link").type("강추!");
cy.get(".restaurant-add-form").submit();
cy.get(".restaurant").should("have.length", 1);
});
});
5 changes: 0 additions & 5 deletions cypress/e2e/spec.cy.js

This file was deleted.

Loading