diff --git a/.gitignore b/.gitignore index 49b960da7..356a301c1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ db.json # test file cypress/fixtures/ -cypress/videos/ \ No newline at end of file +cypress/videos/ +cypress/screenshots/ \ No newline at end of file diff --git a/src/css/global.css b/src/css/global.css index 5db220250..fb41b5919 100644 --- a/src/css/global.css +++ b/src/css/global.css @@ -10,4 +10,7 @@ --button-hover-color: rgba(0, 188, 212, 0.16); --primary-text-color: rgba(0, 0, 0, 0.87); --list-border-color: #dcdcdc; + --snackbar-bg-color: #333; + --snackbar-text-color: #fff; + --snackbar-error-bg-color: rgb(177, 28, 28); } diff --git a/src/css/index.css b/src/css/index.css index 605226ee4..10130682e 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -15,6 +15,15 @@ a { text-decoration: none; } +li { + list-style-type: none; + text-align: center; + font-style: normal; + display: flex; + justify-content: center; + border-bottom: 1px solid var(--list-border-color); +} + #app { width: 40%; min-width: 465px; @@ -165,10 +174,6 @@ input:focus { height: 35px; } -.single-input { - width: 300px; -} - .single-input-container { margin: auto; } @@ -206,7 +211,9 @@ h4 { align-items: center; width: 100%; } -#product-list-wrapper ul { + +#product-list-wrapper ul, +#change-list-wrapper ul { padding: 0; } @@ -217,19 +224,6 @@ h4 { width: 100%; } -#change-list-wrapper ul { - padding: 0; -} - -li { - list-style-type: none; - text-align: center; - font-style: normal; - display: flex; - justify-content: center; - border-bottom: 1px solid var(--list-border-color); -} - #product-list { width: 100%; } @@ -279,7 +273,8 @@ li { margin-top: -8px; } -#change-list li { +#change-list li, +.single-input { width: 300px; } diff --git a/src/css/snackbar.css b/src/css/snackbar.css index d651cfdbf..bae63ce8c 100644 --- a/src/css/snackbar.css +++ b/src/css/snackbar.css @@ -12,8 +12,8 @@ visibility: hidden; min-width: 250px; max-width: 300px; - background-color: #333; - color: #fff; + background-color: var(--snackbar-bg-color); + color: var(--snackbar-text-color); text-align: center; border-radius: 2px; padding: 16px; @@ -22,7 +22,7 @@ } .error { - background-color: rgb(177, 28, 28); + background-color: var(--snackbar-error-bg-color); } .show { diff --git a/src/js/__test__/vendingMachine.test.js b/src/js/__test__/vendingMachine.test.js index 6cc56799f..9ee37ea02 100644 --- a/src/js/__test__/vendingMachine.test.js +++ b/src/js/__test__/vendingMachine.test.js @@ -1,5 +1,5 @@ import { ERROR_MESSAGE } from '../constants'; -import vendingMachine from '../model/VendingMachine'; +import vendingMachine from '../model/vendingMachine_Test'; describe('자판기 기본 기능 테스트', () => { describe('자판기 상품 추가 기능 테스트', () => { diff --git a/src/js/api/requestLogin.ts b/src/js/api/requestLogin.ts index 148223b7e..7e1597dd3 100644 --- a/src/js/api/requestLogin.ts +++ b/src/js/api/requestLogin.ts @@ -1,17 +1,14 @@ -import { ALERT_MESSAGE, ERROR_MESSAGE, SERVER_URL } from '../constants'; +import { ERROR_MESSAGE } from '../constants'; +import { LoginSuccess } from '../interfaces/apiStatus.interface'; +import ApiWrapper from '../utils/ApiWrapper'; -const requestLogin = async (accountData: Object) => { - const response = await fetch(SERVER_URL + '/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(accountData), - }); +const apiWrapper = new ApiWrapper(); - const dataResult = await response.json(); +const requestLogin = async (accountData: Object) => { + const response = await apiWrapper.post('/login', accountData); + const dataResult: LoginSuccess | string = await response.json(); - if (!response.ok) { + if (typeof dataResult === 'string') { switch (dataResult) { case 'Cannot find user': throw new Error(ERROR_MESSAGE.USER_IS_NOT_EXIST); @@ -21,9 +18,7 @@ const requestLogin = async (accountData: Object) => { throw new Error(dataResult); } - localStorage.setItem('accessToken', dataResult.accessToken); - localStorage.setItem('user', JSON.stringify(dataResult.user)); - return ALERT_MESSAGE.LOGIN_SUCCESS(dataResult.user.name); + return dataResult; }; export default requestLogin; diff --git a/src/js/api/requestModifyUserData.ts b/src/js/api/requestModifyUserData.ts index 2cc2a01ba..0dc851075 100644 --- a/src/js/api/requestModifyUserData.ts +++ b/src/js/api/requestModifyUserData.ts @@ -1,18 +1,14 @@ -import { ALERT_MESSAGE, ERROR_MESSAGE, SERVER_URL } from '../constants'; +import { ERROR_MESSAGE } from '../constants'; import { User } from '../interfaces/UserData.interface'; +import ApiWrapper from '../utils/ApiWrapper'; -const requestModifyUserData = async (userData: User) => { - const response = await fetch(SERVER_URL + `/users/${userData.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(userData), - }); +const apiWrapper = new ApiWrapper(); - const dataResult = await response.json(); +const requestModifyUserData = async (userData: User) => { + const response = await apiWrapper.put(`/users/${userData.id}`, userData); + const dataResult: User | string = await response.json(); - if (!response.ok) { + if (typeof dataResult === 'string') { switch (dataResult) { case 'Password is too short': throw new Error(ERROR_MESSAGE.PASSWORD_IS_TOO_SHORT); @@ -20,14 +16,7 @@ const requestModifyUserData = async (userData: User) => { throw new Error(dataResult); } - const updatedInfo = { - email: dataResult.email, - name: dataResult.name, - id: dataResult.id, - }; - - localStorage.setItem('user', JSON.stringify(updatedInfo)); - return ALERT_MESSAGE.USER_INFO_MODIFY_SUCCESS; + return dataResult; }; export default requestModifyUserData; diff --git a/src/js/api/requestRegister.ts b/src/js/api/requestRegister.ts index 396a8064d..a4236a0cf 100644 --- a/src/js/api/requestRegister.ts +++ b/src/js/api/requestRegister.ts @@ -1,14 +1,11 @@ -import { ALERT_MESSAGE, ERROR_MESSAGE, SERVER_URL } from '../constants'; +import { ERROR_MESSAGE } from '../constants'; import { User } from '../interfaces/UserData.interface'; +import ApiWrapper from '../utils/ApiWrapper'; + +const apiWrapper = new ApiWrapper(); const requestRegister = async (userData: User) => { - const response = await fetch(SERVER_URL + '/users', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(userData), - }); + const response = await apiWrapper.post('/users', userData); if (!response.ok) { const errorMessage = await response.json(); @@ -21,7 +18,7 @@ const requestRegister = async (userData: User) => { throw new Error(errorMessage); } - return ALERT_MESSAGE.REGISTER_SUCCESS; + return true; }; export default requestRegister; diff --git a/src/js/components/AddChangeComponent.ts b/src/js/components/AddChangeComponent.ts index dea2532d8..ef8efca24 100644 --- a/src/js/components/AddChangeComponent.ts +++ b/src/js/components/AddChangeComponent.ts @@ -1,18 +1,21 @@ +import coinModel from '../model/CoinModel'; import vendingMachine from '../model/VendingMachine'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; +import * as Component from './abstractComponents/Component'; -class AddChangeComponent { +class AddChangeComponent extends Component.DynamicComponent { $changeAddForm: HTMLElement; $totalChange: HTMLElement; noticeStateChanged: Function; parentElement: HTMLElement; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; } - private bindEventAndElement = () => { + protected bindEventAndElement = () => { this.$totalChange = document.querySelector('#total-change'); this.$changeAddForm = document.querySelector('#change-add-form'); this.$changeAddForm.addEventListener('submit', this.onSubmitChangeAdd); @@ -28,7 +31,7 @@ class AddChangeComponent { }; refreshChange = () => { - this.$totalChange.textContent = vendingMachine.getTotalMoney().toString(); + this.$totalChange.textContent = coinModel.getCoinsValue(vendingMachine.getVendingMachineMoney()).toString(); }; render = () => { @@ -36,8 +39,8 @@ class AddChangeComponent { this.bindEventAndElement(); }; - private template = () => ` -
+ protected template = () => ` +

자판기가 보유할 금액을 입력해주세요

{ + protected bindEventAndElement = () => { this.$productAddForm = this.parentElement.querySelector('#product-add-form'); this.$productList = this.parentElement.querySelector('#product-list'); @@ -34,14 +36,12 @@ class AddProductComponent { } }; - refreshComponent = () => {}; - render = () => { this.parentElement.insertAdjacentHTML('beforeend', this.template()); this.bindEventAndElement(); }; - private template = () => ` + protected template = () => `

추가할 상품 정보를 입력해주세요.

diff --git a/src/js/components/ChangeListComponent.ts b/src/js/components/ChangeListComponent.ts index 12892ee1d..297807e32 100644 --- a/src/js/components/ChangeListComponent.ts +++ b/src/js/components/ChangeListComponent.ts @@ -1,6 +1,7 @@ import vendingMachine from '../model/VendingMachine'; +import * as Component from './abstractComponents/Component'; -class ChangeListComponent { +class ChangeListComponent extends Component.DynamicComponent { $changeList: HTMLElement; $amountCoin500: HTMLElement; $amountCoin100: HTMLElement; @@ -9,10 +10,11 @@ class ChangeListComponent { parentElement: HTMLElement; constructor(parentElement: HTMLElement) { + super(); this.parentElement = parentElement; } - private bindElement = () => { + protected bindEventAndElement = () => { this.$changeList = document.querySelector('#change-list'); this.$amountCoin500 = document.querySelector('#amount-coin-500'); this.$amountCoin100 = document.querySelector('#amount-coin-100'); @@ -21,7 +23,7 @@ class ChangeListComponent { }; refreshChange = () => { - const { coin10, coin50, coin100, coin500 } = vendingMachine.getChanges(); + const { coin10, coin50, coin100, coin500 } = vendingMachine.getVendingMachineMoney(); this.$amountCoin500.textContent = `${coin500}개`; this.$amountCoin100.textContent = `${coin100}개`; @@ -31,10 +33,10 @@ class ChangeListComponent { render = () => { this.parentElement.insertAdjacentHTML('beforeend', this.template()); - this.bindElement(); + this.bindEventAndElement(); }; - private template = () => ` + protected template = () => `

자판기가 보유한 동전

    diff --git a/src/js/components/InputMoneyComponent.ts b/src/js/components/InputMoneyComponent.ts index f209abf92..7686c3b1e 100644 --- a/src/js/components/InputMoneyComponent.ts +++ b/src/js/components/InputMoneyComponent.ts @@ -1,18 +1,20 @@ import vendingMachine from '../model/VendingMachine'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; +import * as Component from './abstractComponents/Component'; -class InputMoneyComponent { +class InputMoneyComponent extends Component.DynamicComponent { $inputMoneyForm: HTMLElement; $totalMoney: HTMLElement; noticeStateChanged: Function; parentElement: HTMLElement; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; } - private bindEventAndElement = () => { + protected bindEventAndElement = () => { this.$totalMoney = document.querySelector('#total-money'); this.$inputMoneyForm = document.querySelector('#input-money-form'); this.$inputMoneyForm.addEventListener('submit', this.onSubmitInputMoney); @@ -28,7 +30,7 @@ class InputMoneyComponent { }; refreshChange = () => { - this.$totalMoney.textContent = vendingMachine.getUserMoney().toString(); + this.$totalMoney.textContent = vendingMachine.getUserInputMoney().toString(); }; render = () => { @@ -36,7 +38,7 @@ class InputMoneyComponent { this.bindEventAndElement(); }; - private template = () => ` + protected template = () => `

    상품을 구매할 금액을 투입해주세요

    diff --git a/src/js/components/LoginFormComponent.ts b/src/js/components/LoginFormComponent.ts index 2d3c14087..a22c1debe 100644 --- a/src/js/components/LoginFormComponent.ts +++ b/src/js/components/LoginFormComponent.ts @@ -1,9 +1,14 @@ +import { LogInAccount } from '../interfaces/UserData.interface'; +import { LoginSuccess } from '../interfaces/apiStatus.interface'; +import { ALERT_MESSAGE, PATH_NAME } from '../constants'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; import router from '../routes'; -import { PATH_NAME } from '../constants'; +import Store from '../utils/Store'; import requestLogin from '../api/requestLogin'; +import * as Component from './abstractComponents/Component'; -class LoginFormComponent { +class LoginFormComponent extends Component.StaticComponent { + store: Store; parentElement: HTMLElement; noticeStateChanged: Function; $loginInputSection: HTMLElement; @@ -12,29 +17,41 @@ class LoginFormComponent { $mainContents: HTMLElement; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); + this.store = new Store(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; } - private bindEventAndElement = () => { - this.$loginInputSection = this.parentElement.querySelector('#login-input-container'); - this.$loginForm = document.querySelector('#login-form'); - this.$registerLink = document.querySelector('#register-link'); - this.$mainContents = document.querySelector('.main-contents'); + protected bindEventAndElement = () => { + this.store.setVariable([ + { name: '$loginInputSection', selector: '#login-input-container' }, + { name: '$loginForm', selector: '#login-form' }, + { name: '$registerLink', selector: '#register-link' }, + { name: '$mainContents', selector: '.main-contents' }, + ]); - this.$loginForm.addEventListener('submit', this.onSubmitLogin); - this.$registerLink.addEventListener('click', this.onClickRegister); + this.store.get('$loginForm').addEventListener('submit', this.onSubmitLogin); + this.store.get('$registerLink').addEventListener('click', this.onClickRegister); + + // this.$loginInputSection = this.parentElement.querySelector('#login-input-container'); + // this.$loginForm = document.querySelector('#login-form'); + // this.$registerLink = document.querySelector('#register-link'); + // this.$mainContents = document.querySelector('.main-contents'); + + // this.$loginForm.addEventListener('submit', this.onSubmitLogin); + // this.$registerLink.addEventListener('click', this.onClickRegister); }; private onSubmitLogin = async (e: SubmitEvent) => { e.preventDefault(); - const accountData = { - email: (this.$loginForm.querySelector('#email-input')).value, - password: (this.$loginForm.querySelector('#password-input')).value, + const accountData: LogInAccount = { + email: (this.store.get('$loginForm').querySelector('#email-input')).value, + password: (this.store.get('$loginForm').querySelector('#password-input')).value, }; - if (await throwableFunctionHandler(() => requestLogin(accountData))) { + if (await throwableFunctionHandler(async () => this.onLogIn(accountData))) { this.noticeStateChanged(); } }; @@ -44,15 +61,22 @@ class LoginFormComponent { router.go(PATH_NAME.REGISTER); }; - refreshComponent = () => {}; + private onLogIn = async (accountData: LogInAccount) => { + const data: LoginSuccess = await requestLogin(accountData); + + localStorage.setItem('accessToken', data.accessToken); + localStorage.setItem('user', JSON.stringify(data.user)); + + return ALERT_MESSAGE.LOGIN_SUCCESS(data.user.name); + }; render = () => { this.parentElement.insertAdjacentHTML('beforeend', this.template()); this.bindEventAndElement(); - this.$mainContents.replaceChildren(); + this.store.get('$mainContents').replaceChildren(); }; - template = () => `

    로그인

    + protected template = () => `

    로그인

    diff --git a/src/js/components/ModifyProductComponent.ts b/src/js/components/ModifyProductComponent.ts index 008b02bac..fa8f767eb 100644 --- a/src/js/components/ModifyProductComponent.ts +++ b/src/js/components/ModifyProductComponent.ts @@ -2,8 +2,9 @@ import vendingMachine from '../model/VendingMachine'; import ProductItemComponent from './ProductItemComponent'; import { Product } from '../interfaces/VendingMachine.interface'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; +import * as Component from './abstractComponents/Component'; -class ModifyProductComponent { +class ModifyProductComponent extends Component.DependentComponent { name: string; price: number; amount: number; @@ -11,13 +12,14 @@ class ModifyProductComponent { $productList: HTMLElement; constructor(parentElement: HTMLElement) { + super(); this.parentElement = parentElement; } - bindEvent = () => { + bindEventAndElement() { this.$productList = this.parentElement.querySelector('#product-list'); this.$productList.addEventListener('click', this.onSubmitModifyCompleteButton); - }; + } private onSubmitModifyCompleteButton = async (e: PointerEvent) => { if ((e.target).className !== 'product-modify-submit-button') { @@ -35,15 +37,16 @@ class ModifyProductComponent { const prevName = (parentList.querySelector('.product-modify-submit-button')).dataset.name; if (await throwableFunctionHandler(() => vendingMachine.modifyProduct(prevName, product))) { - ul.replaceChild(this.replaceList(product, ProductItemComponent), parentList); + ul.replaceChild(this.replaceList(product), parentList); } }; - private replaceList = (product: Product, component: Function) => { + private replaceList = (product: Product) => { const fragment = new DocumentFragment(); const li = document.createElement('li'); + const productItemComponent = new ProductItemComponent(product, true); - li.insertAdjacentHTML('beforeend', component(product, true)); + li.insertAdjacentHTML('beforeend', productItemComponent.render()); fragment.appendChild(li); return fragment; @@ -56,7 +59,7 @@ class ModifyProductComponent { return this.template(); }; - private template = () => ` + protected template = () => ` { - return ` -`; -}; +class ProductItemComponent extends Component.DependentComponent { + bindEventAndElement(): void { + throw new Error('Method not implemented.'); + } + name: string; + price: number; + amount: number; + isAdmin: boolean; -const purchaseButton = () => { - return ``; -}; + constructor(product: Product, isAdmin: boolean = false) { + super(); + this.name = product.name; + this.price = product.price; + this.amount = product.amount; + this.isAdmin = isAdmin; + } -const ProductItemComponent = (product: Product, isAdmin: boolean = false) => { - const { name, price, amount } = product; - return ` - ${name} - ${price} - ${amount} - - ${isAdmin ? controlButton() : purchaseButton()} - - `; -}; + render() { + return this.template(); + } + + protected template = () => ` + ${this.name} + ${this.price} + ${this.amount} + + ${this.isAdmin ? this.controlButton() : this.purchaseButton()} + + `; + + controlButton = () => { + return ` + `; + }; + + purchaseButton = () => { + return ``; + }; +} export default ProductItemComponent; diff --git a/src/js/components/ProductListComponent.ts b/src/js/components/ProductListComponent.ts index a66405916..366b232b9 100644 --- a/src/js/components/ProductListComponent.ts +++ b/src/js/components/ProductListComponent.ts @@ -4,20 +4,22 @@ import ProductItemComponent from './ProductItemComponent'; import { Product } from '../interfaces/VendingMachine.interface'; import { REMOVE_CONFIRM_MESSAGE } from '../constants'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; +import * as Component from './abstractComponents/Component'; -class ProductListComponent { +class ProductListComponent extends Component.DynamicComponent { ModifyProductComponent: ModifyProductComponent; parentElement: HTMLElement; noticeStateChanged: Function; $productList: HTMLElement; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; this.ModifyProductComponent = new ModifyProductComponent(parentElement); } - private bindEvent = () => { + protected bindEventAndElement = () => { this.$productList = this.parentElement.querySelector('#product-list'); this.$productList.addEventListener('click', this.onClickModifyButton); this.$productList.addEventListener('click', this.onClickRemoveButton); @@ -37,7 +39,7 @@ class ProductListComponent { }; ul.replaceChild(this.replaceList(product, this.ModifyProductComponent.render), oldLi); - this.ModifyProductComponent.bindEvent(); + this.ModifyProductComponent.bindEventAndElement(); }; private onClickRemoveButton = async (e: PointerEvent) => { @@ -70,13 +72,14 @@ class ProductListComponent { addProductItem(product: Product) { const fragment = new DocumentFragment(); const li = document.createElement('li'); + const productItemComponent = new ProductItemComponent(product, true); - li.insertAdjacentHTML('beforeend', ProductItemComponent(product, true)); + li.insertAdjacentHTML('beforeend', productItemComponent.render()); fragment.appendChild(li); this.$productList.appendChild(fragment); } - refreshComponent = () => { + refreshChange = () => { const products = vendingMachine.getProducts(); products.forEach(product => { @@ -86,10 +89,10 @@ class ProductListComponent { render = () => { this.parentElement.insertAdjacentHTML('beforeend', this.template()); - this.bindEvent(); + this.bindEventAndElement(); }; - private template = () => ` + protected template = () => `

    상품 현황

    diff --git a/src/js/components/PurchasableProductListComponent.ts b/src/js/components/PurchasableProductListComponent.ts index 7471feebd..06f9c1c91 100644 --- a/src/js/components/PurchasableProductListComponent.ts +++ b/src/js/components/PurchasableProductListComponent.ts @@ -2,18 +2,20 @@ import vendingMachine from '../model/VendingMachine'; import ProductItemComponent from './ProductItemComponent'; import { Product } from '../interfaces/VendingMachine.interface'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; +import * as Component from './abstractComponents/Component'; -class ProductListComponent { +class ProductListComponent extends Component.DynamicComponent { parentElement: HTMLElement; noticeStateChanged: Function; $productList: HTMLElement; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; } - private bindEvent = () => { + protected bindEventAndElement = () => { this.$productList = this.parentElement.querySelector('#product-list'); this.$productList.addEventListener('click', this.onClickPurchaseButton); }; @@ -37,13 +39,14 @@ class ProductListComponent { addProductItem(product: Product) { const fragment = new DocumentFragment(); const li = document.createElement('li'); + const productItemComponent = new ProductItemComponent(product); - li.insertAdjacentHTML('beforeend', ProductItemComponent(product)); + li.insertAdjacentHTML('beforeend', productItemComponent.render()); fragment.appendChild(li); this.$productList.appendChild(fragment); } - refreshComponent = () => { + refreshChange = () => { const products = vendingMachine.getProducts(); products.forEach(product => { @@ -53,12 +56,12 @@ class ProductListComponent { render = () => { this.parentElement.insertAdjacentHTML('beforeend', this.template()); - this.bindEvent(); + this.bindEventAndElement(); }; - private template = () => ` + protected template = () => `
    -
    +

    구매 가능 상품 현황

    • diff --git a/src/js/components/RegisterFormComponent.ts b/src/js/components/RegisterFormComponent.ts index fd67a91b8..cba1d83fa 100644 --- a/src/js/components/RegisterFormComponent.ts +++ b/src/js/components/RegisterFormComponent.ts @@ -1,9 +1,11 @@ import requestRegister from '../api/requestRegister'; +import { ALERT_MESSAGE } from '../constants'; import { User } from '../interfaces/UserData.interface'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; import { checkUserDataValidate } from '../utils/userValidation'; +import * as Component from './abstractComponents/Component'; -class RegisterFormComponent { +class RegisterFormComponent extends Component.StaticComponent { parentElement: HTMLElement; noticeStateChanged: Function; $loginInputSection: HTMLElement; @@ -11,11 +13,12 @@ class RegisterFormComponent { $mainContents: HTMLElement; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; } - private bindEventAndElement = () => { + protected bindEventAndElement = () => { this.$loginInputSection = this.parentElement.querySelector('#login-input-container'); this.$registerForm = document.querySelector('#register-form'); this.$mainContents = document.querySelector('.main-contents'); @@ -40,7 +43,9 @@ class RegisterFormComponent { }; private checkValidateAndRequest = (userData: User) => { - return checkUserDataValidate(userData) && requestRegister(userData); + checkUserDataValidate(userData); + requestRegister(userData); + return ALERT_MESSAGE.REGISTER_SUCCESS; }; render = () => { @@ -49,7 +54,7 @@ class RegisterFormComponent { this.$mainContents.replaceChildren(); }; - template = () => `

      회원가입

      + protected template = () => `

      회원가입

      diff --git a/src/js/components/ReturnChangeComponent.ts b/src/js/components/ReturnChangeComponent.ts index 110c1447d..1009b54d1 100644 --- a/src/js/components/ReturnChangeComponent.ts +++ b/src/js/components/ReturnChangeComponent.ts @@ -1,7 +1,8 @@ import vendingMachine from '../model/VendingMachine'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; +import * as Component from './abstractComponents/Component'; -class ReturnChangeComponent { +class ReturnChangeComponent extends Component.DynamicComponent { $changeList: HTMLElement; $returnChangeButton: HTMLElement; $amountCoin500: HTMLElement; @@ -12,11 +13,12 @@ class ReturnChangeComponent { noticeStateChanged: Function; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; } - private bindElementAndEvent = () => { + protected bindEventAndElement = () => { this.$changeList = document.querySelector('#change-list'); this.$returnChangeButton = document.querySelector('#return-change-button'); this.$amountCoin500 = document.querySelector('#amount-coin-500'); @@ -34,7 +36,7 @@ class ReturnChangeComponent { }; refreshChange = () => { - const { coin10, coin50, coin100, coin500 } = vendingMachine.getUserChanges(); + const { coin10, coin50, coin100, coin500 } = vendingMachine.getUserMoney(); this.$amountCoin500.textContent = `${coin500}개`; this.$amountCoin100.textContent = `${coin100}개`; this.$amountCoin50.textContent = `${coin50}개`; @@ -43,10 +45,10 @@ class ReturnChangeComponent { render = () => { this.parentElement.insertAdjacentHTML('beforeend', this.template()); - this.bindElementAndEvent(); + this.bindEventAndElement(); }; - private template = () => ` + protected template = () => `

      잔돈 반환

        diff --git a/src/js/components/UserInfoComponent.ts b/src/js/components/UserInfoComponent.ts index 4e38dee4a..f606152f6 100644 --- a/src/js/components/UserInfoComponent.ts +++ b/src/js/components/UserInfoComponent.ts @@ -1,12 +1,13 @@ import requestModifyUserData from '../api/requestModifyUserData'; -import { PATH_NAME } from '../constants'; +import { ALERT_MESSAGE, PATH_NAME } from '../constants'; import { User } from '../interfaces/UserData.interface'; import routes from '../routes'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; import { getUserData } from '../utils/userAction'; import { checkUserDataValidate } from '../utils/userValidation'; +import * as Component from './abstractComponents/Component'; -class UserInfoComponent { +class UserInfoComponent extends Component.StaticComponent { parentElement: HTMLElement; noticeStateChanged: Function; $loginInputSection: HTMLElement; @@ -16,11 +17,12 @@ class UserInfoComponent { user: User; constructor(parentElement: HTMLElement, noticeStateChanged: Function) { + super(); this.parentElement = parentElement; this.noticeStateChanged = noticeStateChanged; } - private bindEventAndElement = () => { + protected bindEventAndElement = () => { this.$loginInputSection = this.parentElement.querySelector('#login-input-container'); this.$userInfoForm = document.querySelector('#user-info-form'); this.$mainContents = document.querySelector('.main-contents'); @@ -48,7 +50,18 @@ class UserInfoComponent { }; private checkValidateAndRequest = async (userData: User) => { - return checkUserDataValidate(userData) && (await requestModifyUserData(userData)); + checkUserDataValidate(userData); + const data = await requestModifyUserData(userData); + + const updatedInfo = { + email: data.email, + name: data.name, + id: data.id, + }; + + localStorage.setItem('user', JSON.stringify(updatedInfo)); + + return ALERT_MESSAGE.USER_INFO_MODIFY_SUCCESS; }; private onClickCloseButton = () => { @@ -62,7 +75,7 @@ class UserInfoComponent { this.$mainContents.replaceChildren(); }; - template = () => `

        회원 정보 수정

        + protected template = () => `

        회원 정보 수정

        diff --git a/src/js/components/abstractComponents/Component.ts b/src/js/components/abstractComponents/Component.ts new file mode 100644 index 000000000..b39f8db76 --- /dev/null +++ b/src/js/components/abstractComponents/Component.ts @@ -0,0 +1,17 @@ +export abstract class DynamicComponent { + protected abstract bindEventAndElement(): void; + abstract render(): void; + abstract refreshChange(): void; + protected abstract template(): string; +} + +export abstract class DependentComponent { + abstract bindEventAndElement(): void; + protected abstract template(): string; +} + +export abstract class StaticComponent { + protected abstract bindEventAndElement(): void; + abstract render(): void; + protected abstract template(): string; +} diff --git a/src/js/components/mainContentsComponent.ts b/src/js/components/mainContentsComponent.ts index af74a3c84..f4a7dfb54 100644 --- a/src/js/components/mainContentsComponent.ts +++ b/src/js/components/mainContentsComponent.ts @@ -3,17 +3,19 @@ import router from '../routes'; import Auth from '../utils/Auth'; import throwableFunctionHandler from '../utils/throwableFunctionHandler'; import { getUserInitial, logout } from '../utils/userAction'; +import * as Component from './abstractComponents/Component'; -export default class MainContentsComponent { +export default class MainContentsComponent extends Component.StaticComponent { $mainContents: HTMLElement; isLogin: Boolean; constructor() { + super(); this.$mainContents = document.querySelector('.main-contents'); this.isLogin = Auth(); } - private bindElementAndEvent = () => { + protected bindEventAndElement() { const productManageButton = document.querySelector('#product-manage-button'); const changeAddButton = document.querySelector('#change-add-button'); const productPurchaseButton = document.querySelector('#product-purchase-button'); @@ -56,15 +58,15 @@ export default class MainContentsComponent { loginPageButton.addEventListener('click', () => { router.go(PATH_NAME.LOGIN); }); - }; + } render = () => { this.$mainContents.replaceChildren(); this.$mainContents.insertAdjacentHTML('beforeend', this.template()); - this.bindElementAndEvent(); + this.bindEventAndElement(); }; - private template = () => ` + protected template = () => `
        ${this.isLogin ? this.LoginStateMenu(getUserInitial()) : this.LogoutStateMenu()}
        diff --git a/src/js/constants.ts b/src/js/constants.ts index 845af1d32..a96bb7290 100644 --- a/src/js/constants.ts +++ b/src/js/constants.ts @@ -63,6 +63,6 @@ const ALERT_MESSAGE = { const REMOVE_CONFIRM_MESSAGE = '정말로 삭제하시겠습니까?'; const DEV_MODE = false; -const SERVER_URL = 'http://usage-json-server.herokuapp.com'; +const SERVER_URL = 'https://usage-json-server.herokuapp.com'; export { PATH_NAME, RULES, ERROR_MESSAGE, REMOVE_CONFIRM_MESSAGE, ALERT_MESSAGE, DEV_MODE, SERVER_URL }; diff --git a/src/js/interfaces/UserData.interface.ts b/src/js/interfaces/UserData.interface.ts index 90b3b65f7..665b0fc24 100644 --- a/src/js/interfaces/UserData.interface.ts +++ b/src/js/interfaces/UserData.interface.ts @@ -6,10 +6,15 @@ interface User { passwordCheck: string; } +interface LogInAccount { + email: string; + password: string; +} + interface AccessToken { email: string; iat: number; exp: number; } -export type { User, AccessToken }; +export type { User, AccessToken, LogInAccount }; diff --git a/src/js/interfaces/apiStatus.interface.ts b/src/js/interfaces/apiStatus.interface.ts new file mode 100644 index 000000000..c6d6afaa9 --- /dev/null +++ b/src/js/interfaces/apiStatus.interface.ts @@ -0,0 +1,8 @@ +import { User } from './UserData.interface'; + +interface LoginSuccess { + accessToken: string; + user: User; +} + +export type { LoginSuccess }; diff --git a/src/js/model/CoinModel.ts b/src/js/model/CoinModel.ts new file mode 100644 index 000000000..8262d2421 --- /dev/null +++ b/src/js/model/CoinModel.ts @@ -0,0 +1,122 @@ +import { ALERT_MESSAGE, ERROR_MESSAGE, RULES } from '../constants'; +import { Coin } from '../interfaces/VendingMachine.interface'; +import { getRandomInt } from '../utils/utils'; + +class CoinModel { + protected availableCoinTypeList: Array; + + constructor() { + this.availableCoinTypeList = [500, 100, 50, 10, 0]; + } + + getCoinsValue(coins: Coin) { + return coins.coin10 * 10 + coins.coin50 * 50 + coins.coin100 * 100 + coins.coin500 * 500; + } + + makeChangesToCoin(inputMoney: number, vendingMachineChanges: Coin) { + const coin = this.getRandomChangeCoin(inputMoney); + inputMoney -= coin; + + switch (coin) { + case 500: + vendingMachineChanges.coin500 += 1; + break; + case 100: + vendingMachineChanges.coin100 += 1; + break; + case 50: + vendingMachineChanges.coin50 += 1; + break; + case 10: + vendingMachineChanges.coin10 += 1; + break; + } + + if (inputMoney >= RULES.MINIMUM_CHANGE) { + this.makeChangesToCoin(inputMoney, vendingMachineChanges); + } + } + + returnChanges(userMoney: number, setUserMoney: Function, userChanges: Coin, vendingMachineChanges: Coin) { + const coin = this.getChangeCoin(userMoney, vendingMachineChanges); + setUserMoney(userMoney - coin); + userMoney -= coin; + + switch (coin) { + case 500: + vendingMachineChanges.coin500 -= 1; + userChanges.coin500 += 1; + break; + case 100: + vendingMachineChanges.coin100 -= 1; + userChanges.coin100 += 1; + break; + case 50: + vendingMachineChanges.coin50 -= 1; + userChanges.coin50 += 1; + break; + case 10: + vendingMachineChanges.coin10 -= 1; + userChanges.coin10 += 1; + break; + case 0: + if (userMoney > 0) { + throw new Error(ERROR_MESSAGE.NOT_ENOUGH_RETURN_CHANGE); + } + break; + } + + if (userMoney >= RULES.MINIMUM_CHANGE) { + this.returnChanges(userMoney, setUserMoney, userChanges, vendingMachineChanges); + } + + return ALERT_MESSAGE.RETURN_CHARGE_SUCCESS; + } + + private getChangeCoin(vendingMachinemoney: number, vendingMachineChanges: Coin) { + const coins = this.availableCoinTypeList.filter(coin => { + if (vendingMachinemoney < coin) { + return false; + } + + switch (coin) { + case 500: + if (vendingMachineChanges.coin500 > 0) { + return true; + } + break; + case 100: + if (vendingMachineChanges.coin100 > 0) { + return true; + } + break; + case 50: + if (vendingMachineChanges.coin50 > 0) { + return true; + } + break; + case 10: + if (vendingMachineChanges.coin10 > 0) { + return true; + } + break; + case 0: + return true; + } + + return false; + }); + + return coins[0]; + } + + private getRandomChangeCoin(money: number) { + const coins = this.availableCoinTypeList.filter(coin => coin <= money); + const index = getRandomInt(coins.length); + return coins[index]; + } +} + +const coinModel = new CoinModel(); + +export default coinModel; diff --git a/src/js/model/VendingMachine.ts b/src/js/model/VendingMachine.ts index 044bbc38b..6ee55ea36 100644 --- a/src/js/model/VendingMachine.ts +++ b/src/js/model/VendingMachine.ts @@ -1,6 +1,7 @@ import { ALERT_MESSAGE, ERROR_MESSAGE, RULES } from '../constants'; -import { Product, Coin } from '../interfaces/VendingMachine.interface'; -import { getRandomInt } from '../utils/utils'; +import { Coin, Product } from '../interfaces/VendingMachine.interface'; +import CoinModel from './CoinModel'; + import { isValidProductPrice, isValidProductAmount, @@ -10,42 +11,38 @@ import { isDuplicatedName, } from './validator'; -class VendingMachine { - private products: Array; - private changes: Coin; - private userChanges: Coin; - private totalMoney: number; - private userMoney: number; - private availableCoinTypeList: Array; +export class VendingMachine { + protected products: Array; + protected userMoney: Coin; + protected vendingMachineMoney: Coin; + protected userInputMoney: number; constructor() { this.products = []; - this.availableCoinTypeList = [500, 100, 50, 10, 0]; - this.changes = { coin10: 0, coin50: 0, coin100: 0, coin500: 0 }; - this.userChanges = { coin10: 0, coin50: 0, coin100: 0, coin500: 0 }; - this.totalMoney = 0; - this.userMoney = 0; + this.userInputMoney = 0; + this.userMoney = { coin10: 0, coin50: 0, coin100: 0, coin500: 0 }; + this.vendingMachineMoney = { coin10: 0, coin50: 0, coin100: 0, coin500: 0 }; } getProducts() { return this.products; } - getChanges() { - return this.changes; + getUserMoney() { + return this.userMoney; } - getUserChanges() { - return this.userChanges; + getUserInputMoney() { + return this.userInputMoney; } - getTotalMoney() { - return this.totalMoney; + getVendingMachineMoney() { + return this.vendingMachineMoney; } - getUserMoney() { - return this.userMoney; - } + setUserMoney = (money: number) => { + this.userInputMoney = money; + }; addProduct(product: Product) { this.checkProductValidate(product); @@ -77,58 +74,23 @@ class VendingMachine { inputChanges(money: number) { this.checkInputChangesValidate(money); - this.totalMoney += money; - this.makeChangesToCoin(money); + CoinModel.makeChangesToCoin(money, this.vendingMachineMoney); + return ALERT_MESSAGE.ADD_CHARGE_SUCCESS(money); } inputUserMoney(money: number) { this.checkInputMoneyValidate(money); - this.userMoney += money; + this.userInputMoney += money; return ALERT_MESSAGE.INPUT_MONEY_SUCCESS(money); } - returnChanges() { - const coin = this.getChangeCoin(this.userMoney); - this.userMoney -= coin; - - switch (coin) { - case 500: - this.changes.coin500 -= 1; - this.userChanges.coin500 += 1; - break; - case 100: - this.changes.coin100 -= 1; - this.userChanges.coin100 += 1; - break; - case 50: - this.changes.coin50 -= 1; - this.userChanges.coin50 += 1; - break; - case 10: - this.changes.coin10 -= 1; - this.userChanges.coin10 += 1; - break; - case 0: - if (this.userMoney > 0) { - throw new Error(ERROR_MESSAGE.NOT_ENOUGH_RETURN_CHANGE); - } - break; - } - - if (this.userMoney >= RULES.MINIMUM_CHANGE) { - this.returnChanges(); - } - - return ALERT_MESSAGE.RETURN_CHARGE_SUCCESS; - } - purchaseProduct(productName: string) { const productIndex = this.findProductIndex(productName); const productPrice = this.products[productIndex].price; const productAmount = this.products[productIndex].amount; - if (this.userMoney < productPrice) { + if (this.userInputMoney < productPrice) { throw new Error(ERROR_MESSAGE.NOT_ENOUGH_MONEY); } @@ -136,77 +98,14 @@ class VendingMachine { throw new Error(ERROR_MESSAGE.NOT_ENOUGH_AMOUNT); } - this.userMoney -= productPrice; + this.userInputMoney -= productPrice; this.products[productIndex].amount -= 1; return ALERT_MESSAGE.PURCHASE_PRODUCT_SUCCESS(productName); } - private makeChangesToCoin(money: number) { - const coin = this.getRandomChangeCoin(money); - money -= coin; - - switch (coin) { - case 500: - this.changes.coin500 += 1; - break; - case 100: - this.changes.coin100 += 1; - break; - case 50: - this.changes.coin50 += 1; - break; - case 10: - this.changes.coin10 += 1; - break; - } - - if (money >= RULES.MINIMUM_CHANGE) { - this.makeChangesToCoin(money); - } - } - - private getChangeCoin(money: number) { - const coins = this.availableCoinTypeList.filter(coin => { - if (money < coin) { - return false; - } - - switch (coin) { - case 500: - if (this.changes.coin500 > 0) { - return true; - } - break; - case 100: - if (this.changes.coin100 > 0) { - return true; - } - break; - case 50: - if (this.changes.coin50 > 0) { - return true; - } - break; - case 10: - if (this.changes.coin10 > 0) { - return true; - } - break; - case 0: - return true; - } - - return false; - }); - - return coins[0]; - } - - private getRandomChangeCoin(money: number) { - const coins = this.availableCoinTypeList.filter(coin => coin <= money); - const index = getRandomInt(coins.length); - return coins[index]; + returnChanges() { + return CoinModel.returnChanges(this.userInputMoney, this.setUserMoney, this.userMoney, this.vendingMachineMoney); } private checkProductValidate(product: Product, originalIndex: number = RULES.NOT_EXIST_INDEX) { @@ -236,7 +135,7 @@ class VendingMachine { throw new Error(ERROR_MESSAGE.IS_NOT_UNIT_OF_TEN); } - if (this.totalMoney + money > RULES.MAX_VENDING_MACHINE_CHANGE) { + if (CoinModel.getCoinsValue(this.vendingMachineMoney) + money > RULES.MAX_VENDING_MACHINE_CHANGE) { throw new Error(ERROR_MESSAGE.TOO_MUCH_VENDING_MACHINE_CHANGE); } } @@ -250,6 +149,12 @@ class VendingMachine { throw new Error(ERROR_MESSAGE.IS_NOT_UNIT_OF_TEN); } +<<<<<<< HEAD + if (this.userInputMoney + money > RULES.MAX_VENDING_MACHINE_INPUT_MONEY) { + throw new Error(ERROR_MESSAGE.TOO_MUCH_VENDING_MACHINE_INPUT_MONEY); + } + } +======= if (this.userMoney + money > RULES.MAX_VENDING_MACHINE_INPUT_MONEY) { throw new Error(ERROR_MESSAGE.TOO_MUCH_VENDING_MACHINE_INPUT_MONEY); } @@ -263,6 +168,7 @@ class VendingMachine { this.totalMoney = 0; this.userMoney = 0; } +>>>>>>> 59bf0111a48ba352d7e652e94949ec58bf554d7c } const vendingMachine = new VendingMachine(); diff --git a/src/js/model/VendingMachine_Test.ts b/src/js/model/VendingMachine_Test.ts new file mode 100644 index 000000000..34eb9b1b6 --- /dev/null +++ b/src/js/model/VendingMachine_Test.ts @@ -0,0 +1,19 @@ +import { VendingMachine } from './VendingMachine'; + +class VendingMachine_Test extends VendingMachine { + constructor() { + super(); + } + + /* 테스트 용도로 작성된 초기화 함수입니다. 실제 로직에선 사용되지 않습니다. */ + initialize() { + this.products = []; + this.userInputMoney = 0; + this.userMoney = { coin10: 0, coin50: 0, coin100: 0, coin500: 0 }; + this.vendingMachineMoney = { coin10: 0, coin50: 0, coin100: 0, coin500: 0 }; + } +} + +const vendingMachineTest = new VendingMachine_Test(); + +export default vendingMachineTest; diff --git a/src/js/pages/Login.ts b/src/js/pages/Login.ts index 94fe1833b..2fd54e1e9 100644 --- a/src/js/pages/Login.ts +++ b/src/js/pages/Login.ts @@ -13,7 +13,6 @@ export default class Login { render = () => { this.LoginFormComponent.render(); - this.LoginFormComponent.refreshComponent(); }; private locationChange = () => { diff --git a/src/js/pages/ProductManage.ts b/src/js/pages/ProductManage.ts index 53b19b4d0..6e1646dad 100644 --- a/src/js/pages/ProductManage.ts +++ b/src/js/pages/ProductManage.ts @@ -33,7 +33,6 @@ export default class ProductManage { return; } - this.AddProductComponent.refreshComponent(); - this.ProductListComponent.refreshComponent(); + this.ProductListComponent.refreshChange(); }; } diff --git a/src/js/pages/ProductPurchase.ts b/src/js/pages/ProductPurchase.ts index e34b2d957..79c84b613 100644 --- a/src/js/pages/ProductPurchase.ts +++ b/src/js/pages/ProductPurchase.ts @@ -28,8 +28,9 @@ export default class ProductPurchase { this.InputMoneyComponent.render(); this.PurchasableProductListComponent.render(); this.ReturnChangeComponent.render(); + this.InputMoneyComponent.refreshChange(); - this.PurchasableProductListComponent.refreshComponent(); + this.PurchasableProductListComponent.refreshChange(); this.ReturnChangeComponent.refreshChange(); }; diff --git a/src/js/pages/UserInfo.ts b/src/js/pages/UserInfo.ts index ad9a7c537..d0eed5fc9 100644 --- a/src/js/pages/UserInfo.ts +++ b/src/js/pages/UserInfo.ts @@ -1,6 +1,4 @@ import UserInfoComponent from '../components/UserInfoComponent'; -import { PATH_NAME } from '../constants'; -import routes from '../routes'; export default class UserInfo { UserInfoComponent: UserInfoComponent; diff --git a/src/js/utils/ApiWrapper.ts b/src/js/utils/ApiWrapper.ts new file mode 100644 index 000000000..779d1f749 --- /dev/null +++ b/src/js/utils/ApiWrapper.ts @@ -0,0 +1,33 @@ +import { SERVER_URL } from '../constants'; + +class ApiWrapper { + SERVER_URL: string; + header: HeadersInit; + + constructor() { + this.SERVER_URL = SERVER_URL; + this.header = { 'Content-Type': 'application/json' }; + } + + async post(path: string, bodyData: Object) { + const response = await fetch(this.SERVER_URL + path, { + method: 'POST', + headers: this.header, + body: JSON.stringify(bodyData), + }); + + return response; + } + + async put(path: string, bodyData: Object) { + const response = await fetch(this.SERVER_URL + path, { + method: 'PUT', + headers: this.header, + body: JSON.stringify(bodyData), + }); + + return response; + } +} + +export default ApiWrapper; diff --git a/src/js/utils/Store.ts b/src/js/utils/Store.ts new file mode 100644 index 000000000..70707dc20 --- /dev/null +++ b/src/js/utils/Store.ts @@ -0,0 +1,23 @@ +type DOMVariable = { + name: string; + selector: string; +}; + +export default class Store { + private variable: DOMVariable[]; + + constructor() { + this.variable = []; + } + + setVariable(array: Array): void { + array.forEach(item => { + this.variable.push({ name: item.name, selector: item.selector }); + }); + } + + get(name: string): HTMLElement { + const targetItem = this.variable.find(item => item.name === name); + return document.querySelector(targetItem.selector); + } +} diff --git a/src/js/utils/snackbar.ts b/src/js/utils/snackbar.ts index 648fc7ce9..26b8e450e 100644 --- a/src/js/utils/snackbar.ts +++ b/src/js/utils/snackbar.ts @@ -1,11 +1,16 @@ class Snackbar { $snackbar: HTMLElement; + timerId: any; constructor() { this.$snackbar = document.querySelector('.snackbar'); + this.timerId = null; } - async push(msg: string | Error) { + push = (msg: string | Error) => { + if (this.timerId) { + clearTimeout(this.timerId); + } if (typeof msg === 'object') { this.$snackbar.classList.add('error'); msg = msg.message; @@ -15,10 +20,11 @@ class Snackbar { this.$snackbar.textContent = msg; this.$snackbar.classList.add('show'); - setTimeout(() => { + this.timerId = setTimeout(() => { this.$snackbar.classList.toggle('show'); + this.timerId = null; }, 3000); - } + }; } const snackbar = new Snackbar();