diff --git a/README.md b/README.md index e97a1d649..3f72aab87 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,23 @@ ## πŸš€ κΈ°λŠ₯ μš”κ΅¬μ‚¬ν•­ ### μ§€ν•˜μ²  μ—­ κ΄€λ ¨ κΈ°λŠ₯ + - μ§€ν•˜μ²  역을 λ“±λ‘ν•˜κ³  μ‚­μ œν•  수 μžˆλ‹€. (단, 노선에 λ“±λ‘λœ 역은 μ‚­μ œν•  수 μ—†λ‹€) - μ€‘λ³΅λœ μ§€ν•˜μ²  μ—­ 이름이 등둝될 수 μ—†λ‹€. - μ§€ν•˜μ²  역은 2κΈ€μž 이상이어야 ν•œλ‹€. - μ§€ν•˜μ²  μ—­μ˜ λͺ©λ‘μ„ μ‘°νšŒν•  수 μžˆλ‹€. ### μ§€ν•˜μ²  λ…Έμ„  κ΄€λ ¨ κΈ°λŠ₯ + - μ§€ν•˜μ²  노선을 λ“±λ‘ν•˜κ³  μ‚­μ œν•  수 μžˆλ‹€. - μ€‘λ³΅λœ μ§€ν•˜μ²  λ…Έμ„  이름이 등둝될 수 μ—†λ‹€. - λ…Έμ„  등둝 μ‹œ 상행 쒅점역과 ν•˜ν–‰ 쒅점역을 μž…λ ₯λ°›λŠ”λ‹€. - μ§€ν•˜μ²  λ…Έμ„ μ˜ λͺ©λ‘μ„ μ‘°νšŒν•  수 μžˆλ‹€. ### μ§€ν•˜μ²  ꡬ간 μΆ”κ°€ κΈ°λŠ₯ + - μ§€ν•˜μ²  노선에 ꡬ간을 μΆ”κ°€ν•˜λŠ” κΈ°λŠ₯은 노선에 역을 μΆ”κ°€ν•˜λŠ” κΈ°λŠ₯이라고도 ν•  수 μžˆλ‹€. - - μ—­κ³Ό 역사이λ₯Ό ꡬ간이라 ν•˜κ³  이 κ΅¬κ°„λ“€μ˜ λͺ¨μŒμ΄ 노선이닀. + - μ—­κ³Ό 역사이λ₯Ό ꡬ간이라 ν•˜κ³  이 κ΅¬κ°„λ“€μ˜ λͺ¨μŒμ΄ 노선이닀. - ν•˜λ‚˜μ˜ 역은 μ—¬λŸ¬κ°œμ˜ 노선에 좔가될 수 μžˆλ‹€. - μ—­κ³Ό μ—­ 사이에 μƒˆλ‘œμš΄ 역이 μΆ”κ°€ 될 수 μžˆλ‹€. - λ…Έμ„ μ—μ„œ κ°ˆλž˜κΈΈμ€ 생길 수 μ—†λ‹€. @@ -24,6 +27,7 @@ ### μ§€ν•˜μ²  ꡬ간 μ‚­μ œ κΈ°λŠ₯ + - 노선에 λ“±λ‘λœ 역을 μ œκ±°ν•  수 μžˆλ‹€. - 쒅점을 μ œκ±°ν•  경우 λ‹€μŒ 역이 쒅점이 λœλ‹€. - 노선에 ν¬ν•¨λœ 역이 λ‘κ°œ μ΄ν•˜μΌ λ•ŒλŠ” 역을 μ œκ±°ν•  수 μ—†λ‹€. @@ -31,6 +35,7 @@ ### μ§€ν•˜μ²  노선에 λ“±λ‘λœ μ—­ 쑰회 κΈ°λŠ₯ + - λ…Έμ„ μ˜ 상행 쒅점뢀터 ν•˜ν–‰ μ’…μ κΉŒμ§€ μ—°κ²°λœ μˆœμ„œλŒ€λ‘œ μ—­ λͺ©λ‘μ„ μ‘°νšŒν•  수 μžˆλ‹€.
@@ -38,32 +43,38 @@ ## πŸ’» ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ κ²°κ³Ό ### 역관리 + ### 노선관리 + ### ꡬ간관리 + ### 노선도 좜λ ₯ - + ## βœ… ν”„λ‘œκ·Έλž˜λ° μš”κ΅¬μ‚¬ν•­ ### 메뉴 λ²„νŠΌ + - μ—­ 관리 button νƒœκ·ΈλŠ” `#station-manager-button` id값을 κ°€μ§„λ‹€. - λ…Έμ„  관리 button νƒœκ·ΈλŠ” `#line-manager-button` id값을 κ°€μ§„λ‹€. - ꡬ간 관리 button νƒœκ·ΈλŠ” `#section-manager-button` id값을 κ°€μ§„λ‹€. - μ§€ν•˜μ²  노선도 좜λ ₯ 관리 button νƒœκ·ΈλŠ” `#map-print-manager-button` id값을 κ°€μ§„λ‹€. ### μ§€ν•˜μ²  μ—­ κ΄€λ ¨ κΈ°λŠ₯ + - μ§€ν•˜μ²  역을 μž…λ ₯ν•˜λŠ” input νƒœκ·ΈλŠ” `#station-name-input` id값을 κ°€μ§„λ‹€. - μ§€ν•˜μ²  역을 μΆ”κ°€ν•˜λŠ” button νƒœκ·ΈλŠ” `#station-add-button` id값을 κ°€μ§„λ‹€. - μ§€ν•˜μ²  역을 μ‚­μ œν•˜λŠ” button νƒœκ·ΈλŠ” `.station-delete-button` class값을 κ°€μ§„λ‹€. ### μ§€ν•˜μ²  λ…Έμ„  κ΄€λ ¨ κΈ°λŠ₯ + - μ§€ν•˜μ²  λ…Έμ„ μ˜ 이름을 μž…λ ₯ν•˜λŠ” input νƒœκ·ΈλŠ” `#line-name-input` id값을 κ°€μ§„λ‹€. - μ§€ν•˜μ²  λ…Έμ„ μ˜ 상행 쒅점을 μ„ νƒν•˜λŠ” select νƒœκ·ΈλŠ” `#line-start-station-selector` id값을 κ°€μ§„λ‹€. - μ§€ν•˜μ²  λ…Έμ„ μ˜ ν•˜ν–‰ 쒅점을 μ„ νƒν•˜λŠ” select νƒœκ·ΈλŠ” `#line-end-station-selector` id값을 κ°€μ§„λ‹€. @@ -71,6 +82,7 @@ - μ§€ν•˜μ²  노선을 μ‚­μ œν•˜λŠ” button νƒœκ·ΈλŠ” `.line-delete-button` class값을 κ°€μ§„λ‹€. ### μ§€ν•˜μ²  ꡬ간 μΆ”κ°€ κΈ°λŠ₯ + - μ§€ν•˜μ²  노선을 μ„ νƒν•˜λŠ” button νƒœκ·ΈλŠ” `.section-line-menu-button` class값을 κ°€μ§„λ‹€. - μ§€ν•˜μ²  ꡬ간을 μ„€μ •ν•  μ—­ select νƒœκ·ΈλŠ” `#section-station-selector` id값을 κ°€μ§„λ‹€. - μ§€ν•˜μ²  κ΅¬κ°„μ˜ μˆœμ„œλ₯Ό μž…λ ₯ν•˜λŠ” input νƒœκ·ΈλŠ” `#section-order-input` id값을 κ°€μ§„λ‹€. @@ -78,6 +90,7 @@ - μ§€ν•˜μ²  ꡬ간을 μ œκ±°ν•˜λŠ” button νƒœκ·ΈλŠ” `.section-delete-button` class값을 κ°€μ§„λ‹€. ### μ§€ν•˜μ²  노선도 좜λ ₯ κΈ°λŠ₯ + - μ§€ν•˜μ²  노선도 좜λ ₯ λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ `
` νƒœκ·Έλ₯Ό λ§Œλ“€κ³  ν•΄λ‹Ή νƒœκ·Έ 내뢀에 노선도λ₯Ό 좜λ ₯ν•œλ‹€. ### κΈ°μ‘΄ μš”κ΅¬μ‚¬ν•­ @@ -101,7 +114,8 @@ - [https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Template_literals](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Template_literals) ### μΆ”κ°€λœ μš”κ΅¬μ‚¬ν•­ -- [data](https://developer.mozilla.org/ko/docs/Learn/HTML/Howto/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%86%8D%EC%84%B1_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0)속성을 ν™œμš©ν•˜μ—¬ html νƒœκ·Έμ— μ—­, λ…Έμ„ , κ΅¬κ°„μ˜ μœ μΌν•œ 데이터 값듀을 κ΄€λ¦¬ν•œλ‹€. + +- [data](https://developer.mozilla.org/ko/docs/Learn/HTML/Howto/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%86%8D%EC%84%B1_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0)속성을 ν™œμš©ν•˜μ—¬ html νƒœκ·Έμ— μ—­, λ…Έμ„ , κ΅¬κ°„μ˜ μœ μΌν•œ 데이터 값듀을 κ΄€λ¦¬ν•œλ‹€. - [localStorage](https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage)λ₯Ό μ΄μš©ν•˜μ—¬, μƒˆλ‘œκ³ μΉ¨ν•˜λ”λΌλ„ κ°€μž₯ μ΅œκ·Όμ— μž‘μ—…ν•œ 정보듀을 뢈러올 수 μžˆλ„λ‘ ν•œλ‹€.
@@ -112,3 +126,56 @@ - **κΈ°λŠ₯을 κ΅¬ν˜„ν•˜κΈ° 전에 javascript-subway-precourse/docs/README.md νŒŒμΌμ— κ΅¬ν˜„ν•  κΈ°λŠ₯ λͺ©λ‘**을 정리해 μΆ”κ°€ν•œλ‹€. - **git의 commit λ‹¨μœ„λŠ” μ•ž λ‹¨κ³„μ—μ„œ README.md νŒŒμΌμ— μ •λ¦¬ν•œ κΈ°λŠ₯ λͺ©λ‘ λ‹¨μœ„λ‘œ μΆ”κ°€**ν•œλ‹€. - [ν”„λ¦¬μ½”μŠ€ 과제 제좜](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) λ¬Έμ„œ 절차λ₯Ό 따라 λ―Έμ…˜μ„ μ œμΆœν•œλ‹€. + +## πŸ”₯ κΈ°λŠ₯ λͺ©λ‘ + +- 곡톡 κΈ°λŠ₯ + + - [x] 각 메뉴λ₯Ό ν΄λ¦­μ‹œ ν•΄λ‹Ή κΈ°λŠ₯을 μˆ˜ν–‰ν•  수 μžˆκ²Œν•΄μ•Ό ν•œλ‹€. (μ—­ 관리, λ…Έμ„  관리, ꡬ간 관리, μ§€ν•˜μ²  노선도 좜λ ₯) + - [x] dataλ₯Ό κ³΅μœ ν•  수 μžˆλ„λ‘ 각 λ©”λ‰΄μ—μ„œ dataλ₯Ό 동기화 μ‹œν‚¨λ‹€. + - [x] localStorageλ₯Ό μ΄μš©ν•˜μ—¬ μƒˆλ‘œκ³ μΉ¨ν•΄λ„ 졜근 데이터λ₯Ό ν™œμš©ν•΄ λͺ©λ‘λ“€μ„ ν‘œμ‹œν•œλ‹€. + - [x] form 제좜 μ‹œ input을 μ΄ˆκΈ°ν™” ν•œλ‹€. + - [x] μ˜¬λ°”λ₯΄ μ•Šμ€ μž…λ ₯일 경우 경고창을 ν‘œμ‹œν•˜μ—¬ μž¬μž…λ ₯ ν•  수 μžˆκ²Œν•œλ‹€. + +- μ—­ κ΄€λ ¨ κΈ°λŠ₯ + + - [x] μ‚¬μš©μžκ°€ μ—­ 이름을 μΆ”κ°€ν•  수 μžˆκ²Œν•΄μ•Ό ν•œλ‹€. + - [x] μ—­ 이름이 μ€‘λ³΅λ˜λŠ”μ§€ ν™•μΈν•΄μ•Όν•œλ‹€. + - [x] μ—­ 이름이 2κΈ€μž 이상인지 확인해야 ν•œλ‹€. (미만일 경우 κ²½κ³ μ°½ ν‘œμ‹œ) + - [x] μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 역듀을 μ°¨λ‘€λŒ€λ‘œ 좜λ ₯ν•΄μ•Ό ν•œλ‹€. + - [x] μ‚­μ œ λ²„νŠΌμ„ ν΄λ¦­μ‹œ μ‚­μ œν•  수 있게 ν•΄μ•Όν•œλ‹€. + - [x] μ‚­μ œμ— μ„±κ³΅ν–ˆμ„ 경우 λͺ©λ‘μ„ μ—…λ°μ΄νŠΈν•΄μ„œ λ‹€μ‹œ ν‘œμ‹œν•΄μ•Ό ν•œλ‹€. + - [x] μ‚­μ œμ‹œ μ‚¬μš©μžμ˜ 확인을 λ°›λŠ”λ‹€ + - [x] μ‚­μ œ μ‹œ 노선에 μΆ”κ°€λ˜μ–΄ μžˆμ„ 경우 μ‚­μ œν•  수 μ—†κ²Œ ν•΄μ•Ό ν•œλ‹€. + +- λ…Έμ„  κ΄€λ ¨ κΈ°λŠ₯ + + - [x] λ…Έμ„  이름을 등둝 ν•  수 있게 ν•œλ‹€. + - [x] 상행 쒅점 ν•˜ν–‰ 쒅점을 λ“±λ‘ν•œ μ—­ λͺ©λ‘μ—μ„œ 선택할 수 있게 ν•΄μ•Όν•œλ‹€. + - [x] λ…Έμ„  이름이 μ€‘λ³΅λ˜λŠ”μ§€ ν™•μΈν•΄μ•Όν•œλ‹€. + - [x] 상행 쒅점과 ν•˜ν–‰ 쒅점이 같은지 확인해야 ν•œλ‹€. + - [x] λ…Έμ„ λͺ©λ‘μ„ 좜λ ₯ν•œλ‹€. + - [x] μ‚­μ œ λ²„νŠΌ 클릭 μ‹œ μ‚­μ œν•  수 있게 ν•΄μ•Όν•œλ‹€. + - [x] μ‚­μ œμ— μ„±κ³΅ν–ˆμ„ 경우 λͺ©λ‘μ„ μ—…λ°μ΄νŠΈ ν•΄μ„œ λ‹€μ‹œ ν‘œμ‹œν•΄μ•Ό ν•œλ‹€. + - [x] μ‚­μ œ μ‹œ μ‚¬μš©μžμ˜ 확인을 λ°›λŠ”λ‹€. + +- ꡬ간 κ΄€λ ¨ κΈ°λŠ₯ + + - [x] μ‚¬μš©μžκ°€ λ“±λ‘ν•œ 노선을 ν‘œμ‹œν•΄μ•Όν•œλ‹€. + - [x] 노선을 클릭 μ‹œ κ·Έ 노선을 μˆ˜μ • ν•  수 있게 ν•΄μ•Όν•œλ‹€. + - [x] μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 역듀을 μˆœμ„œμ— 따라 μΆ”κ°€ ν•  수 μžˆλ‹€. + - [x] μƒˆλ‘œμš΄ ꡬ간 λ“±λ‘μ‹œ λ…Έμ„ μ˜ 상행쒅점과 ν•˜ν–‰μ’…μ μ„ μ—…λ°μ΄νŠΈ ν•œλ‹€. + - [x] ꡬ간 λ“±λ‘μ‹œ 노선에 이미 ν¬ν•¨λœ 역인지 ν™•μΈν•œλ‹€. + - [x] 'λ…Έμ„ μ—μ„œ 제거' λ²„νŠΌ ν΄λ¦­μ‹œ λ…Έμ„ μ—μ„œ 제거 ν•  수 있게 ν•΄μ•Όν•œλ‹€. + - [x] μ‚­μ œμ‹œ 노선에 ν¬ν•¨λœ 역이 λ‘κ°œ μ΄ν•˜μΈμ§€ ν™•μΈν•΄μ•Όν•œλ‹€. + - [x] μ‚­μ œμ— μ„±κ³΅ν–ˆμ„ 경우 λͺ©λ‘μ„ μ—…λ°μ΄νŠΈ ν•΄μ„œ λ‹€μ‹œ ν‘œμ‹œν•΄μ•Ό ν•œλ‹€. + +- 노선도 좜λ ₯ κΈ°λŠ₯ + + - [x] λ…Έμ„ μ˜ 상행쒅점뢀터 ν•˜ν–‰μ’…μ κΉŒμ§€ μˆœμ„œλŒ€λ‘œ λͺ©λ‘μ„ ν‘œμ‹œν•΄μ•Ό ν•œλ‹€. + +- μ˜ˆμ™Έμ‚¬ν•­ + - μž…λ ₯의 λͺ¨λ“  곡백은 μ œμ™Έν•œλ‹€. + - 노선에 ꡬ간 μΆ”κ°€ μ‹œ ν˜„μž¬μ˜ 길이보닀 더 높은 숫자λ₯Ό μž…λ ₯ν•œ 경우 λ§ˆμ§€λ§‰μ— μΆ”κ°€λœλ‹€. + + diff --git a/index.html b/index.html index fc99deac2..253c80c62 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,108 @@ μ§€ν•˜μ²  노선도 관리 +
-

πŸš‡ μ§€ν•˜μ²  노선도 관리

+

πŸš‡ μ§€ν•˜μ²  노선도 관리

+ +
+
+ + + +
+
+

πŸš‰ μ§€ν•˜μ²  μ—­ λͺ©λ‘

+ + + + + + + + +
μ—­ 이름섀정
+
+
+ + +
diff --git a/src/factory/Component.js b/src/factory/Component.js new file mode 100644 index 000000000..b7e84eb9e --- /dev/null +++ b/src/factory/Component.js @@ -0,0 +1,45 @@ +export default class Component { + constructor(props = {}) { + this.data = {}; + this.props = props; + this.managerId = props.managerId; + this.syncData = props.syncData; + this.container = document.querySelector(`#${props.containerId}`); + } + + setData(nextData) { + this.data = { + ...this.data, + ...nextData, + }; + this.render?.(); + } + + clearInput() { + this.userInput.value = ''; + } + + addToTable(value, keyName) { + const newData = { ...this.data }; + newData[keyName].push(value); + this.syncData(newData); + } + + deleteFromTable(index, keyName) { + const newData = { ...this.data }; + newData[keyName].splice(index, 1); + this.syncData(newData); + } + + updateTable(template) { + this.table.innerHTML = template; + } + + show() { + this.container.style.display = 'block'; + } + + hide() { + this.container.style.display = 'none'; + } +} diff --git a/src/factory/Line.js b/src/factory/Line.js new file mode 100644 index 000000000..e151d227b --- /dev/null +++ b/src/factory/Line.js @@ -0,0 +1,35 @@ +const MIN_SECTION_LENGTH = 2; + +export default class Line { + constructor(props) { + this.props = props; + this.name = props.name; + this.startStation = props.startStation; + this.endStation = props.endStation; + this.section = this.setSection(); + } + + setSection = () => this.props.section || [this.startStation, this.endStation]; + + addStationToSection({ stationName, index }) { + this.section.splice(index, 0, stationName); + this.updateStartStation(); + this.updateEndStation(); + } + + deleteStationFromSection({ index }) { + this.section.splice(index, 1); + this.updateStartStation(); + this.updateEndStation(); + } + + checkSectionLength = () => this.section.length === MIN_SECTION_LENGTH; + + updateStartStation() { + [this.startStation] = this.section; + } + + updateEndStation() { + this.endStation = this.section[this.section.length - 1]; + } +} diff --git a/src/index.js b/src/index.js index e69de29bb..15a4760fc 100644 --- a/src/index.js +++ b/src/index.js @@ -0,0 +1,5 @@ +import SubwayManager from './managers/app.js'; + +const app = new SubwayManager(); + +app.init(); diff --git a/src/managers/LineManager.js b/src/managers/LineManager.js new file mode 100644 index 000000000..44e49518d --- /dev/null +++ b/src/managers/LineManager.js @@ -0,0 +1,98 @@ +import Component from '../factory/Component.js'; +import Line from '../factory/Line.js'; +import { LINE_SELECTOR } from '../share/selector.js'; +import { + isOverlap, + isSameStation, + deleteWhiteSpace, + isEmpty, +} from '../share/utils.js'; +import { lineTableTemplate, optionTemplate } from '../share/template.js'; +import { DATA_KEY, LINE_WORDS } from '../share/words.js'; + +export default class LineManager extends Component { + constructor(props) { + super(props); + + this.form = this.container.querySelector(`#${LINE_SELECTOR.FORM_ID}`); + this.userInput = this.container.querySelector( + `#${LINE_SELECTOR.NAME_INPUT_ID}`, + ); + this.startStationSelector = this.container.querySelector( + `#${LINE_SELECTOR.START_STATION_SELECTOR_ID}`, + ); + this.endStationSelector = this.container.querySelector( + `#${LINE_SELECTOR.END_STATION_SELECTOR}`, + ); + this.table = this.container.querySelector(`#${LINE_SELECTOR.TABLE_BODY}`); + + this.form.addEventListener('submit', this.onSubmit); + this.table.addEventListener('click', this.onTableClick); + } + + onSubmit = (event) => { + event.preventDefault(); + const constructorObj = this.getValues(); + const newLine = new Line(constructorObj); + if (!this.isValid(newLine)) return; + this.addToTable(newLine, DATA_KEY.LINE_LIST); + this.clearInput(); + }; + + onTableClick = (event) => { + const { className } = event.target; + const { index } = event.target.dataset; + if (className !== LINE_SELECTOR.DELETE_BUTTON_CLASS) return; + if (!confirm(LINE_WORDS.CONFIRM_MESSAGE)) return; + this.deleteFromTable(index, DATA_KEY.LINE_LIST); + }; + + isValid({ name, startStation, endStation }) { + if (isEmpty(name)) { + alert(LINE_WORDS.ALERT_MESSAGE_NO_WHITESPACE); + return false; + } + if (isOverlap(name, this.getAllLineNames())) { + alert(LINE_WORDS.ALERT_MESSAGE_SAME_NAME); + return false; + } + if (isSameStation(startStation, endStation)) { + alert(LINE_WORDS.ALERT_MESSAGE_SAME_STATION); + return false; + } + return true; + } + + getAllLineNames = () => this.data.lineList.map((line) => line.name); + + getValues = () => { + const name = deleteWhiteSpace(this.userInput.value); + const { value: startStation } = this.startStationSelector; + const { value: endStation } = this.endStationSelector; + return { name, startStation, endStation }; + }; + + updateStationList() { + this.startStationSelector.innerHTML = this.data.stationList + .map((station) => optionTemplate(station)) + .join(''); + this.endStationSelector.innerHTML = this.data.stationList + .map((station) => optionTemplate(station)) + .join(''); + } + + template() { + return this.data.lineList + .map((line, index) => lineTableTemplate({ + ...line, + index, + buttonClass: LINE_SELECTOR.DELETE_BUTTON_CLASS, + })) + .join(''); + } + + render() { + this.updateStationList(); + this.updateTable(this.template()); + } +} diff --git a/src/managers/MapPrintManager.js b/src/managers/MapPrintManager.js new file mode 100644 index 000000000..77b509eea --- /dev/null +++ b/src/managers/MapPrintManager.js @@ -0,0 +1,22 @@ +import Component from '../factory/Component.js'; +import { MAP_SELECTOR } from '../share/selector.js'; +import { mapTemplate } from '../share/template.js'; + +export default class MapPrintManager extends Component { + constructor(props) { + super(props); + + this.map = document.createElement('div'); + this.map.setAttribute('class', MAP_SELECTOR.MAP_CLASS); + + this.container.appendChild(this.map); + } + + updateTemplate() { + this.map.innerHTML = mapTemplate(this.data.lineList); + } + + render() { + this.updateTemplate(); + } +} diff --git a/src/managers/SectionDetailManager.js b/src/managers/SectionDetailManager.js new file mode 100644 index 000000000..ff9a1c5bc --- /dev/null +++ b/src/managers/SectionDetailManager.js @@ -0,0 +1,100 @@ +import Component from '../factory/Component.js'; +import { SECTION_SELECTOR } from '../share/selector.js'; +import { + optionTemplate, + sectionDetailTableTemplate, +} from '../share/template.js'; +import { isOverlap, isEmpty } from '../share/utils.js'; +import { SECTION_DETAIL_WORDS } from '../share/words.js'; + +export default class SectionDetailManager extends Component { + constructor(props) { + super(props); + + this.form = this.container.querySelector(`#${SECTION_SELECTOR.FORM_ID}`); + this.formHeader = this.container.querySelector( + `#${SECTION_SELECTOR.FORM_HEADER_ID}`, + ); + this.stationSelector = this.container.querySelector( + `#${SECTION_SELECTOR.STATION_SELECTOR_ID}`, + ); + this.userInput = this.container.querySelector( + `#${SECTION_SELECTOR.ORDER_INPUT_ID}`, + ); + this.table = this.container.querySelector( + `#${SECTION_SELECTOR.TABLE_BODY}`, + ); + + this.form.addEventListener('submit', this.onSubmit); + this.table.addEventListener('click', this.onTableClick); + } + + onSubmit = (event) => { + event.preventDefault(); + const targetLine = this.data.currentLineData; + if (!this.isValid(targetLine)) return; + targetLine.addStationToSection(this.getValues()); + this.clearInput(); + this.syncData(this.data); + }; + + onTableClick = (event) => { + const { className } = event.target; + const { index } = event.target.dataset; + const targetLine = this.data.currentLineData; + if (className !== SECTION_SELECTOR.DELETE_BUTTON_CLASS) return; + if (!confirm(SECTION_DETAIL_WORDS.CONFIRM_MESSAGE)) return; + if (targetLine.checkSectionLength()) { + alert(SECTION_DETAIL_WORDS.ALERT_MESSAGE_SECTION_MINLENGTH); + return; + } + targetLine.deleteStationFromSection({ index }); + this.syncData(this.data); + }; + + isValid(targetLine) { + const { stationName, index } = this.getValues(); + if (isEmpty(index)) { + alert(SECTION_DETAIL_WORDS.ALERT_MESSAGE_NO_INDEX); + return false; + } + if (isOverlap(stationName, targetLine.section)) { + alert(SECTION_DETAIL_WORDS.ALERT_MESSAGE_SECTION_INCLUDES_STATION); + return false; + } + return true; + } + + getValues() { + const { value: stationName } = this.stationSelector; + const { value: index } = this.userInput; + return { stationName, index }; + } + + updateOptions() { + this.stationSelector.innerHTML = this.data.stationList + .map((station) => optionTemplate(station)) + .join(''); + } + + updateFormHeader() { + const { name } = this.data.currentLineData; + this.formHeader.innerHTML = `${name} 관리`; + } + + template() { + return this.data.currentLineData.section + .map((station, index) => sectionDetailTableTemplate({ + name: station, + index, + buttonClass: SECTION_SELECTOR.DELETE_BUTTON_CLASS, + })) + .join(''); + } + + render() { + this.updateOptions(); + this.updateFormHeader(); + this.updateTable(this.template()); + } +} diff --git a/src/managers/SectionManager.js b/src/managers/SectionManager.js new file mode 100644 index 000000000..efe9d44ac --- /dev/null +++ b/src/managers/SectionManager.js @@ -0,0 +1,43 @@ +import Component from '../factory/Component.js'; +import SectionDetailManager from './SectionDetailManager.js'; +import { SECTION_SELECTOR } from '../share/selector.js'; + +export default class SectionManager extends Component { + constructor(props) { + super(props); + + this.sectionLineMenu = this.container.querySelector( + `#${SECTION_SELECTOR.LINE_MENU_ID}`, + ); + this.sectionDetailManager = new SectionDetailManager({ + containerId: SECTION_SELECTOR.DETAIL_CONTAINER_ID, + syncData: this.syncData, + }); + + this.sectionLineMenu.addEventListener('click', this.changeSectionDetailManger); + } + + changeSectionDetailManger = (event) => { + if (event.target.nodeName !== 'BUTTON') return; + const { name: selectedLineName } = event.target.dataset; + const newData = { + ...this.data, + currentLineData: + this.getSelectedLine(selectedLineName), + }; + this.syncData(newData); + this.sectionDetailManager.show(); + }; + + getSelectedLine = (name) => this.data.lineList.find((line) => line.name === name); + + updateMenuList() { + this.sectionLineMenu.innerHTML = this.data.lineList + .map((line) => ``) + .join(''); + } + + render() { + this.updateMenuList(); + } +} diff --git a/src/managers/StationManager.js b/src/managers/StationManager.js new file mode 100644 index 000000000..7fff3e87a --- /dev/null +++ b/src/managers/StationManager.js @@ -0,0 +1,79 @@ +import Component from '../factory/Component.js'; +import { STATION_SELECTOR } from '../share/selector.js'; +import { + isOverlap, + isValidLength, + deleteWhiteSpace, +} from '../share/utils.js'; +import { stationTableTemplate } from '../share/template.js'; +import { DATA_KEY, STATION_WORDS } from '../share/words.js'; + +const MIN_STATION_NAME_LENGTH = 2; + +export default class StationManager extends Component { + constructor(props) { + super(props); + + this.userInput = this.container.querySelector( + `#${STATION_SELECTOR.NAME_INPUT_ID}`, + ); + this.form = this.container.querySelector(`#${STATION_SELECTOR.FORM_ID}`); + this.table = this.container.querySelector( + `#${STATION_SELECTOR.TABLE_BODY}`, + ); + + this.form.addEventListener('submit', this.onSubmit); + this.table.addEventListener('click', this.onTableClick); + } + + onSubmit = (event) => { + event.preventDefault(); + const stationName = deleteWhiteSpace(this.userInput.value); + if (!this.isValid(stationName)) return; + this.addToTable(stationName, DATA_KEY.STATION_LIST); + this.clearInput(); + }; + + onTableClick = (event) => { + const { className } = event.target; + const { index } = event.target.dataset; + const { name: stationName } = event.target.parentNode.parentNode.dataset; + if (className !== STATION_SELECTOR.DELETE_BUTTON_CLASS) return; + if (!confirm(STATION_WORDS.CONFIRM_MESSAGE)) return; + if (isOverlap(stationName, this.getAllStationNamesInLines())) { + alert(STATION_WORDS.ALERT_MESSAGE_SECTION_INCLUDES_STATION); + return; + } + this.deleteFromTable(index, DATA_KEY.STATION_LIST); + }; + + isValid(value) { + if (isOverlap(value, this.data.stationList)) { + alert(STATION_WORDS.ALERT_MESSAGE_ALREADY_INCLUDE); + return false; + } + if (!isValidLength(value, MIN_STATION_NAME_LENGTH)) { + alert(STATION_WORDS.ALERT_MESSAGE_STATION_MINLENGTH); + return false; + } + return true; + } + + getAllStationNamesInLines() { + return [...new Set(this.data.lineList.map((line) => line.section).flat())]; + } + + template() { + return this.data.stationList + .map((station, index) => stationTableTemplate({ + stationName: station, + index, + buttonClass: STATION_SELECTOR.DELETE_BUTTON_CLASS, + })) + .join(''); + } + + render() { + this.updateTable(this.template()); + } +} diff --git a/src/managers/app.js b/src/managers/app.js new file mode 100644 index 000000000..6ceb2785b --- /dev/null +++ b/src/managers/app.js @@ -0,0 +1,79 @@ +import Component from '../factory/Component.js'; +import StationManager from './StationManager.js'; +import LineManager from './LineManager.js'; +import SectionManager from './SectionManager.js'; +import MapPrintManager from './MapPrintManager.js'; +import { + MENU, + STATION_SELECTOR, + LINE_SELECTOR, + SECTION_SELECTOR, + MAP_SELECTOR, +} from '../share/selector.js'; +import storage, { STORAGE_KEY } from '../share/storage.js'; + +export default class SubwayManager extends Component { + constructor() { + super(); + this.data = storage.getItem(STORAGE_KEY); + this.menu = document.querySelector(`#${MENU.MENU_CONTAINER_ID}`); + + this.stationManager = new StationManager({ + managerId: MENU.STATION_MANGER_BUTTON_ID, + containerId: STATION_SELECTOR.MANAGER_CONTAINER_ID, + syncData: this.syncData, + }); + this.lineManager = new LineManager({ + managerId: MENU.LINE_MANAGER_BUTTON_ID, + containerId: LINE_SELECTOR.MANAGER_CONTAINER_ID, + syncData: this.syncData, + }); + this.sectionManager = new SectionManager({ + managerId: MENU.SECTION_MANAGER_BUTTON_ID, + containerId: SECTION_SELECTOR.MANAGER_CONTAINER_ID, + syncData: this.syncData, + }); + this.mapPrintManager = new MapPrintManager({ + managerId: MENU.MAP_PRINT_MANAGER_BUTTON_ID, + containerId: MAP_SELECTOR.PRINT_MANAGER_CONTAINER_ID, + syncData: this.syncData, + }); + } + + init() { + this.syncData(this.data); + this.menu.addEventListener('click', this.changeMenu); + } + + changeMenu = (event) => { + const { nodeName } = event.target; + const { id } = event.target; + if (nodeName !== 'BUTTON') return; + this.getManager(id); + }; + + getManager(id) { + const managerList = [ + this.stationManager, + this.lineManager, + this.sectionManager, + this.mapPrintManager, + ]; + managerList.forEach((manager) => { + manager.managerId === id ? manager.show() : manager.hide(); + manager.managerId === MENU.SECTION_MANAGER_BUTTON_ID + ? this.sectionManager.sectionDetailManager.hide() + : ''; + }); + } + + syncData = (data) => { + storage.setItem(STORAGE_KEY, data); + this.setData(data); + this.stationManager.setData(data); + this.lineManager.setData(data); + this.sectionManager.setData(data); + this.sectionManager.sectionDetailManager.setData(data); + this.mapPrintManager.setData(data); + }; +} diff --git a/src/share/selector.js b/src/share/selector.js new file mode 100644 index 000000000..4e7e541c6 --- /dev/null +++ b/src/share/selector.js @@ -0,0 +1,52 @@ +export const MENU = Object.freeze({ + MENU_CONTAINER_ID: 'menu', + STATION_MANGER_BUTTON_ID: 'station-manager-button', + LINE_MANAGER_BUTTON_ID: 'line-manager-button', + SECTION_MANAGER_BUTTON_ID: 'section-manager-button', + MAP_PRINT_MANAGER_BUTTON_ID: 'map-print-manager-button', +}); + +export const STATION_SELECTOR = Object.freeze({ + MANAGER_CONTAINER_ID: 'station-manager', + FORM_ID: 'station-form', + RESULT_CONTAINER_ID: 'station-result', + TABLE: 'station-table', + TABLE_BODY: 'station-table-body', + NAME_INPUT_ID: 'station-name-input', + ADD_BUTTON_ID: 'station-add-button', + DELETE_BUTTON_CLASS: 'station-delete-button', +}); + +export const LINE_SELECTOR = Object.freeze({ + MANAGER_CONTAINER_ID: 'line-manager', + FORM_ID: 'line-form', + NAME_INPUT_ID: 'line-name-input', + START_STATION_SELECTOR_ID: 'line-start-station-selector', + END_STATION_SELECTOR: 'line-end-station-selector', + ADD_BUTTON_ID: 'line-add-button', + DELETE_BUTTON_CLASS: 'line-delete-button', + RESULT_CONTAINER_ID: 'line-result', + TABLE: 'line-table', + TABLE_BODY: 'line-table-body', +}); + +export const SECTION_SELECTOR = Object.freeze({ + MANAGER_CONTAINER_ID: 'section-manager', + LINE_MENU_ID: 'section-line-menu', + LINE_MENU_BUTTON_CLASS: 'section-line-menu-button', + DETAIL_CONTAINER_ID: 'section-detail', + FORM_ID: 'section-form', + FORM_HEADER_ID: 'section-form-header', + STATION_SELECTOR_ID: 'section-station-selector', + ORDER_INPUT_ID: 'section-order-input', + ADD_BUTTON_ID: 'section-add-button', + DELETE_BUTTON_CLASS: 'section-delete-button', + RESULT_CONTAINER_ID: 'section-result', + TABLE: 'section-table', + TABLE_BODY: 'section-table-body', +}); + +export const MAP_SELECTOR = Object.freeze({ + PRINT_MANAGER_CONTAINER_ID: 'map-print-manager', + MAP_CLASS: 'map', +}); diff --git a/src/share/storage.js b/src/share/storage.js new file mode 100644 index 000000000..d6b874d45 --- /dev/null +++ b/src/share/storage.js @@ -0,0 +1,30 @@ +import Line from '../factory/Line.js'; + +export const STORAGE_KEY = 'data'; + +const defaultState = { + stationList: [], + lineList: [], + currentLineData: { + name: '', + section: [], + }, +}; + +const jsonToClassConverter = (dataList, Creator) => dataList.map((data) => new Creator(data)); + +const setItem = (key, data) => localStorage.setItem(key, JSON.stringify(data)); + +const getItem = (key) => { + const tempData = JSON.parse(localStorage.getItem(key)); + const data = tempData || defaultState; + data.lineList = jsonToClassConverter(data.lineList, Line); + return data; +}; + +const storage = { + setItem, + getItem, +}; + +export default storage; diff --git a/src/share/template.js b/src/share/template.js new file mode 100644 index 000000000..00208cad6 --- /dev/null +++ b/src/share/template.js @@ -0,0 +1,37 @@ +export const stationTableTemplate = ({ stationName, index, buttonClass }) => ` + + ${stationName} + + +`; + +export const lineTableTemplate = ({ + name, startStation, endStation, index, buttonClass, +}) => ` + + ${name} + ${startStation} + ${endStation} + + + `; + +export const sectionDetailTableTemplate = ({ index, name, buttonClass }) => ` + + ${index} + ${name} + + + `; + +export const mapTemplate = (lineList) => lineList + .map((line) => ` + `) + .join(''); + +export const optionTemplate = (station) => ``; diff --git a/src/share/utils.js b/src/share/utils.js new file mode 100644 index 000000000..bcd8b00b9 --- /dev/null +++ b/src/share/utils.js @@ -0,0 +1,9 @@ +export const isOverlap = (value, list) => list.includes(value); + +export const isValidLength = (value, minLength) => value.length >= minLength; + +export const isSameStation = (prevStation, nextStation) => prevStation === nextStation; + +export const isEmpty = (value) => !value.length; + +export const deleteWhiteSpace = (words) => words.replaceAll(' ', ''); diff --git a/src/share/words.js b/src/share/words.js new file mode 100644 index 000000000..c76f22668 --- /dev/null +++ b/src/share/words.js @@ -0,0 +1,28 @@ +export const DATA_KEY = { + STATION_LIST: 'stationList', + LINE_LIST: 'lineList', +}; + +export const STATION_WORDS = { + CONFIRM_MESSAGE: 'μ •λ§λ‘œ μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?', + ALERT_MESSAGE_SECTION_INCLUDES_STATION: + '노선에 ν¬ν•¨λ˜μ–΄μžˆμ–΄ μ‚­μ œκ°€ λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€.', + ALERT_MESSAGE_STATION_MINLENGTH: + '역은 곡백을 μ œμ™Έν•˜κ³  2자 이상이여야 ν•©λ‹ˆλ‹€.', + ALERT_MESSAGE_ALREADY_INCLUDE: '이미 λ“±λ‘λœ μ—­μž…λ‹ˆλ‹€.', + +}; + +export const LINE_WORDS = { + CONFIRM_MESSAGE: '정말 노선을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?', + ALERT_MESSAGE_SAME_STATION: '상행 쒅점과 ν•˜ν–‰ 쒅점은 달라야 ν•©λ‹ˆλ‹€.', + ALERT_MESSAGE_SAME_NAME: '이미 λ“±λ‘λœ λ…Έμ„ μ΄λ¦„μž…λ‹ˆλ‹€.', + ALERT_MESSAGE_NO_WHITESPACE: '노선이름은 곡백으둜 μ§€μ •ν•  수 μ—†μŠ΅λ‹ˆλ‹€.', +}; + +export const SECTION_DETAIL_WORDS = { + CONFIRM_MESSAGE: 'μ •λ§λ‘œ μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?', + ALERT_MESSAGE_SECTION_INCLUDES_STATION: '이미 노선에 ν¬ν•¨λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€', + ALERT_MESSAGE_SECTION_MINLENGTH: '노선은 μ΅œμ†Œ 2κ°œμ—­μ„ 포함해야 ν•©λ‹ˆλ‹€.', + ALERT_MESSAGE_NO_INDEX: 'μˆœμ„œλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”', +}; diff --git a/style.css b/style.css new file mode 100644 index 000000000..fa27f7831 --- /dev/null +++ b/style.css @@ -0,0 +1,28 @@ +label { + margin-top: 2rem; + display: block; +} +table, +th, +tr, +td { + border: 1px solid black; +} + +input{ + width: 10rem; +} + +li{ + list-style :none +} + +.dot{ + list-style: disc; +} + +.section-line-menu-button{ + min-width: 3rem; + min-height: 1.5rem; + margin-right: 0.3rem; +} \ No newline at end of file