diff --git a/backend/src/app.ts b/backend/src/app.ts
index 7c53632..ba359bf 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -14,10 +14,12 @@ app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use('/dist', express.static(path.join(__dirname, '../../frontend/dist')));
-app.get('/', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/dist/index.html')));
+app.get('/', (_, res) => res.sendFile(path.join(__dirname, '../../frontend/dist/index.html')));
app.use('/api', apiRouter);
+app.use((_, res) => res.status(404).redirect('/'));
+
app.use(errorMiddleware);
export default app;
diff --git a/frontend/src/assets/images/guest.png b/frontend/src/assets/images/guest.png
new file mode 100644
index 0000000..fd31bd3
Binary files /dev/null and b/frontend/src/assets/images/guest.png differ
diff --git a/frontend/src/assets/images/login-background.png b/frontend/src/assets/images/login-background.png
new file mode 100644
index 0000000..8203a98
Binary files /dev/null and b/frontend/src/assets/images/login-background.png differ
diff --git a/frontend/src/assets/styles/colors.ts b/frontend/src/assets/styles/colors.ts
index cb3ef1a..d18af31 100644
--- a/frontend/src/assets/styles/colors.ts
+++ b/frontend/src/assets/styles/colors.ts
@@ -1,5 +1,7 @@
const colors = {
- primary: '#2AC1BC'
+ primary: '#2AC1BC',
+ offWhite: '#FCFCFC',
+ titleActive: '#1E2222'
};
export default colors;
diff --git a/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts b/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts
index ec16224..79bd0ee 100644
--- a/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts
+++ b/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts
@@ -40,12 +40,14 @@ class MonthlyCashHistoryUIElement extends UIElement {
return;
}
const $date = document.createElement('div');
+ const { date, day, income, expenditure } = cashHistoriesInDay;
+
$date.innerHTML = `
`;
$container.appendChild($date);
diff --git a/frontend/src/ui-elements/color-picker/index.css b/frontend/src/ui-elements/color-picker/index.css
new file mode 100644
index 0000000..ef0265d
--- /dev/null
+++ b/frontend/src/ui-elements/color-picker/index.css
@@ -0,0 +1,54 @@
+.color-picker {
+ width: 100%;
+ display: flex;
+ /* flex-direction: column; */
+ align-items: center;
+ justify-content: space-between;
+ margin: 10px 0;
+}
+
+.color-picker__palette {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 45%;
+}
+
+.color-picker__color {
+ width: 25px;
+ height: 25px;
+ border-radius: 8px;
+ cursor: pointer;
+}
+
+.color-picker__input-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.color-picker__color-hex {
+ background-color: var(--background);
+ width: 120px;
+ height: 30px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ outline: none;
+}
+
+.color-picker__current {
+ width: 25px;
+ height: 25px;
+ border-radius: 8px;
+}
+
+.color-picker__refresh {
+ width: 25px;
+ height: 25px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 10px;
+ font-size: var(--bold-medium);
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/frontend/src/ui-elements/color-picker/index.ts b/frontend/src/ui-elements/color-picker/index.ts
new file mode 100644
index 0000000..603931c
--- /dev/null
+++ b/frontend/src/ui-elements/color-picker/index.ts
@@ -0,0 +1,129 @@
+import colors from '../../assets/styles/colors';
+import UIElement from '../../core/ui-element';
+import { generateRandomColor } from '../../utils/color';
+
+import './index.css';
+
+const COLOR_COUNT = 5;
+
+class ColorPickerUIElement extends UIElement {
+ private colors: string[] = [];
+ private $palette?: HTMLElement;
+ private $colorInput?: HTMLInputElement;
+ private $refreshButton?: HTMLElement;
+
+ constructor ($target: HTMLElement) {
+ super($target, {
+ className: 'color-picker'
+ });
+ }
+
+ get value (): string | undefined {
+ return this.$colorInput?.value;
+ }
+
+ clear (): void {
+ if (this.$colorInput !== undefined) {
+ this.$colorInput.value = '';
+ }
+ }
+
+ private assignColors () {
+ this.colors = [];
+
+ for (let i = 0; i < COLOR_COUNT; i += 1) {
+ this.colors.push(generateRandomColor());
+ }
+ }
+
+ private renderPalette () {
+ if (this.$palette === undefined) {
+ return;
+ }
+
+ this.$palette.innerHTML = this.colors.map((color) => {
+ return `
+
-
${typeData.title}
-
${message}
+
+
${typeData.title}
+
${message}
`;
$toast.classList.add('appear');
diff --git a/frontend/src/view-models/console.ts b/frontend/src/view-models/console.ts
index 262c20e..2accba1 100644
--- a/frontend/src/view-models/console.ts
+++ b/frontend/src/view-models/console.ts
@@ -34,7 +34,7 @@ class ConsoleViewModel extends ViewModel {
private categoriesModel: CategoriesData;
private paymentsModel: PaymentsData;
private focusDateModel: FocusDateData;
- private filteredCashHistoriesModel: CashHistoriesData;
+ private cashHistoriesModel: CashHistoriesData;
private _cashHistoryType: CashHistories = CashHistories.Income;
constructor (view: View) {
@@ -43,7 +43,7 @@ class ConsoleViewModel extends ViewModel {
this.categoriesModel = models.categories;
this.paymentsModel = models.payments;
this.focusDateModel = models.focusDate;
- this.filteredCashHistoriesModel = models.filteredCashHistories;
+ this.cashHistoriesModel = models.cashHistories;
this.initCashHistory();
this.fetchCategories();
@@ -88,7 +88,7 @@ class ConsoleViewModel extends ViewModel {
const date = this.focusDateModel.focusDate;
try {
const histories = await cashHistoryAPI.fetchCashHistories(date.getFullYear(), date.getMonth() + 1);
- this.filteredCashHistoriesModel.cashHistories = histories;
+ this.cashHistoriesModel.cashHistories = histories;
} catch (error) {
const { status } = error;
@@ -127,6 +127,9 @@ class ConsoleViewModel extends ViewModel {
async createPayment (value: string): Promise
{
try {
await paymentAPI.createPayment(value);
+ toast.success('결제수단을 추가했습니다');
+ (this.view as ConsoleView).closeCreatePaymentModal();
+ this.fetchPayments();
} catch (error) {
switch (error.status) {
case 400:
@@ -142,8 +145,6 @@ class ConsoleViewModel extends ViewModel {
break;
}
}
-
- this.fetchPayments();
}
async createCategory (value: string, color?: string): Promise {
@@ -154,7 +155,11 @@ class ConsoleViewModel extends ViewModel {
try {
await categoryAPI.createCategory(value, color, this.cashHistoryType);
+ toast.success('카테고리를 추가했습니다');
+ (this.view as ConsoleView).closeCreateCategoryModal();
+ this.fetchCategories();
} catch (error) {
+ console.log('pass');
switch (error.status) {
case 400:
toast.error('양식을 확인해주세요');
@@ -169,15 +174,13 @@ class ConsoleViewModel extends ViewModel {
break;
}
}
-
- this.fetchCategories();
}
async deleteCategory (id: string): Promise {
try {
await categoryAPI.deleteCategory(Number(id));
-
this.fetchCategories();
+ toast.success('카테고리를 삭제했습니다');
} catch (error) {
const { status } = error;
@@ -190,8 +193,8 @@ class ConsoleViewModel extends ViewModel {
async deletePayment (id: string): Promise {
try {
await paymentAPI.deletePayment(Number(id));
-
this.fetchPayments();
+ toast.success('결제수단을 삭제했습니다');
} catch (error) {
const { status } = error;
@@ -229,6 +232,7 @@ class ConsoleViewModel extends ViewModel {
try {
await cashHistoryAPI.createCashHistory(cashHistoryRequest);
this.fetchCashHistories();
+ toast.success('결제 내역을 추가했습니다');
} catch (error) {
const { status } = error;
@@ -246,6 +250,7 @@ class ConsoleViewModel extends ViewModel {
try {
await cashHistoryAPI.updateCashHistory(id, cashHistoryRequest);
this.fetchCashHistories();
+ toast.success('결제 내역을 수정했습니다');
} catch (error) {
const { status } = error;
diff --git a/frontend/src/view-models/main.ts b/frontend/src/view-models/main.ts
index 9e4c5d9..088afc9 100644
--- a/frontend/src/view-models/main.ts
+++ b/frontend/src/view-models/main.ts
@@ -16,7 +16,7 @@ class MainViewModel extends ViewModel {
private cashHistoriesModel: CashHistoriesData;
private filteredCashHistoriesModel: CashHistoriesData;
private cashHistoryModel: CashHistoryData;
- private filterType: CashHistories | null;
+ private selectedFilter: CashHistories[] = [CashHistories.Income, CashHistories.Expenditure];
constructor (view: View) {
super(view);
@@ -24,30 +24,31 @@ class MainViewModel extends ViewModel {
this.cashHistoriesModel = models.cashHistories;
this.filteredCashHistoriesModel = models.filteredCashHistories;
this.cashHistoryModel = models.cashHistory;
- this.filterType = null;
this.fetchCashHistories();
}
protected subscribe (): void {
- pubsub.subscribe(actions.ON_FOCUS_DATE_CHANGE, async () => {
- await this.fetchCashHistories();
-
- if (this.filterType === null) {
- return;
- }
- this.filterData(this.filterType);
+ pubsub.subscribe(actions.ON_FOCUS_DATE_CHANGE, () => {
+ this.fetchCashHistories()
+ .then(() => {
+ this.applyFilter();
+ });
});
pubsub.subscribe(actions.ON_CASH_HISTORIES_CHANGE, () => {
- this.view.build();
+ this.applyFilter();
});
pubsub.subscribe(actions.ON_FILTERED_CASH_HISTORIES_CHANGE, () => {
this.view.build();
});
- pubsub.subscribe(actions.ON_CASH_HISTORY_CHANGE, () => {
- this.view.build();
+ pubsub.subscribe(actions.ON_CATEGORIES_CHANGE, () => {
+ this.fetchCashHistories();
+ });
+
+ pubsub.subscribe(actions.ON_PAYMENTS_CHANGE, () => {
+ this.fetchCashHistories();
});
}
@@ -57,7 +58,6 @@ class MainViewModel extends ViewModel {
try {
const histories = await cashHistoryAPI.fetchCashHistories(date.getFullYear(), date.getMonth() + 1);
this.cashHistoriesModel.cashHistories = histories;
- this.filteredCashHistoriesModel.cashHistories = histories;
} catch (error) {
const { status } = error;
@@ -67,7 +67,7 @@ class MainViewModel extends ViewModel {
}
}
- filterData (type: number): void {
+ applyFilter (): void {
const { cashHistories } = this.cashHistoriesModel;
if (cashHistories === null) {
return;
@@ -75,8 +75,8 @@ class MainViewModel extends ViewModel {
const filtered = cashHistories.cashHistories.groupedCashHistories.map((monthlyCashHistory) => ({
...monthlyCashHistory,
- cashHistories: monthlyCashHistory.cashHistories.filter(e => e.type === type)
- }));
+ cashHistories: monthlyCashHistory.cashHistories.filter(e => this.selectedFilter.includes(e.type))
+ })).reverse();
this.filteredCashHistoriesModel.cashHistories = {
...cashHistories,
@@ -88,27 +88,17 @@ class MainViewModel extends ViewModel {
}
filterButtonClick (isIncomeChecked: boolean, isExpenditureChecked: boolean): void {
- if (isIncomeChecked && isExpenditureChecked) {
- this.filteredCashHistoriesModel.cashHistories = this.cashHistoriesModel.cashHistories;
- } else if (isIncomeChecked) {
- this.filterType = CashHistories.Income;
- this.filterData(this.filterType);
- } else if (isExpenditureChecked) {
- this.filterType = CashHistories.Expenditure;
- this.filterData(this.filterType);
- } else {
- if (this.filteredCashHistoriesModel.cashHistories === null) {
- return;
- }
- this.filteredCashHistoriesModel.cashHistories = {
- ...this.filteredCashHistoriesModel.cashHistories,
- cashHistories: {
- totalIncome: 0,
- totalExpenditure: 0,
- groupedCashHistories: []
- }
- };
+ this.selectedFilter = [];
+
+ if (isIncomeChecked) {
+ this.selectedFilter.push(CashHistories.Income);
}
+
+ if (isExpenditureChecked) {
+ this.selectedFilter.push(CashHistories.Expenditure);
+ }
+
+ this.applyFilter();
}
onCashHistoryClick (e:Event): void {
diff --git a/frontend/src/views/category-expenditure/index.ts b/frontend/src/views/category-expenditure/index.ts
index 8a23307..b32d782 100644
--- a/frontend/src/views/category-expenditure/index.ts
+++ b/frontend/src/views/category-expenditure/index.ts
@@ -119,7 +119,7 @@ class CategoryExpenditureView extends View {
.reduce((acc, curr) => Math.min(acc, curr.price), max);
const yLabels = new Array(5).fill(0)
- .map((_, i) => `${formatNumber(min + (max / 4) * i)} ₩
`)
+ .map((_, i) => `${formatNumber(Math.round(min + (max / 4) * i))} ₩
`)
.reverse()
.join('');
diff --git a/frontend/src/views/console/index.ts b/frontend/src/views/console/index.ts
index 94ad3b7..7f169e7 100644
--- a/frontend/src/views/console/index.ts
+++ b/frontend/src/views/console/index.ts
@@ -23,6 +23,14 @@ class ConsoleView extends View {
this.consoleViewModel.createOrUpdate();
}
+ closeCreateCategoryModal (): void {
+ this.createCategoryModal?.close();
+ }
+
+ closeCreatePaymentModal (): void {
+ this.createPaymentModal?.close();
+ }
+
enableButton (): void {
$('.console__button')?.classList.add('console__button--active');
}
diff --git a/frontend/src/views/login/index.css b/frontend/src/views/login/index.css
index d01107e..caca58b 100644
--- a/frontend/src/views/login/index.css
+++ b/frontend/src/views/login/index.css
@@ -1,28 +1,34 @@
.login {
--github-color: #171515;
- --title-size: 32px;
+ font-size: var(--body-large);
+ display: flex;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+}
+.login__left {
+ width: 400px;
+ height: 100%;
+ flex-grow: 1;
display: flex;
flex-direction: column;
- align-items: center;
+ align-items: flex-end;
justify-content: center;
- margin-top: 100px;
-}
-
-.login__title {
- font-size: var(--title-size);
- color: var(--title-active);
- font-weight: bold;
+ padding-right: 8vw;
+ padding-bottom: 20vh;
+ background-color: var(--off-white);
}
.login__button {
- margin-top: 20px;
+ z-index: 10;
+ margin-top: 30px;
display: flex;
align-items: center;
justify-content: center;
- width: 300px;
- height: 40px;
- border-radius: 3px;
+ width: 400px;
+ height: 60px;
+ border-radius: 4px;
font-weight: bold;
cursor: pointer;
}
@@ -40,4 +46,39 @@
.login__button--github:hover {
--github-color: #474141;
+}
+
+.login__button--guest {
+ background-color: var(--primary1);
+ color: var(--off-white);
+}
+
+.login__button--guest > img {
+ background-color: var(--off-white);
+ border-radius: 50%;
+}
+
+.login__button--guest:hover {
+ background-color: var(--primary2);
+}
+
+.login__right {
+ width: 600px;
+ height: 100%;
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ background: var(--primary1);
+}
+
+.login__right img {
+ width: 90%;
+ padding-left: 5vw;
+ padding-bottom: 30vh;
+}
+
+.ball {
+ position: absolute;
+ border-radius: 100%;
+ opacity: 0.3;
}
\ No newline at end of file
diff --git a/frontend/src/views/login/index.ts b/frontend/src/views/login/index.ts
index b2eb7a8..8c58150 100644
--- a/frontend/src/views/login/index.ts
+++ b/frontend/src/views/login/index.ts
@@ -2,6 +2,8 @@ import View from '../../core/view';
import { $ } from '../../utils/selector';
import LoginViewModel from '../../view-models/login';
import github from '../../assets/svg/github.svg';
+import guest from '../../assets/images/guest.png';
+import background from '../../assets/images/login-background.png';
import './index.css';
@@ -11,6 +13,49 @@ class LoginView extends View {
constructor ($target: HTMLElement) {
super($target);
this.loginViewModel = new LoginViewModel(this);
+ this.createBackgroundBall();
+ }
+
+ createBackgroundBall (): void {
+ const colors = ['var(--primary1)', 'var(--primary2)', 'var(--off-white)', '#817DCE', '#4CA1DE'];
+
+ const numBalls = 5;
+ const balls = [];
+
+ for (let i = 0; i < numBalls; i++) {
+ const ball = document.createElement('div');
+ ball.classList.add('ball');
+ ball.style.background = colors[Math.floor(Math.random() * colors.length)];
+ ball.style.left = `${Math.floor(Math.random() * 100)}vw`;
+ ball.style.top = `${Math.floor(Math.random() * 100)}vh`;
+ ball.style.transform = `scale(${Math.random()})`;
+ ball.style.width = `${Math.random() * 7}em`;
+ ball.style.height = ball.style.width;
+
+ balls.push(ball);
+ document.body.append(ball);
+ }
+
+ balls.forEach((el, i) => {
+ const to = {
+ x: Math.random() * (i % 2 === 0 ? -11 : 11),
+ y: Math.random() * 12
+ };
+
+ el.animate(
+ [
+ { transform: 'translate(0, 0)' },
+ { transform: `translate(${to.x}rem, ${to.y}rem)` }
+ ],
+ {
+ duration: (Math.random() + 1) * 2000, // random duration
+ direction: 'alternate',
+ fill: 'both',
+ iterations: Infinity,
+ easing: 'ease-in-out'
+ }
+ );
+ });
}
protected addListener (): void {
@@ -20,10 +65,19 @@ class LoginView extends View {
protected render (): void {
this.$target.innerHTML = `