From 3c7f86421b2fbd5558b24fda12a0fe369e7216f5 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Fri, 27 Nov 2020 15:47:11 +0900 Subject: [PATCH 001/281] =?UTF-8?q?[BE]=20TEST:=20jwt=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20test=20code=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/label.api.test.js | 22 +++++++++++----------- server/test/user.api.test.js | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/server/test/label.api.test.js b/server/test/label.api.test.js index 2d040509..e4db62c8 100644 --- a/server/test/label.api.test.js +++ b/server/test/label.api.test.js @@ -36,7 +36,7 @@ describe('label api', () => { try { request(app) .get('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .end((err, res) => { if (err) { throw err; @@ -79,7 +79,7 @@ describe('label api', () => { try { request(app) .post('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -126,7 +126,7 @@ describe('label api', () => { try { request(app) .post('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -150,7 +150,7 @@ describe('label api', () => { try { request(app) .post('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -176,7 +176,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/${seeder.labels[0].id}`) // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -225,7 +225,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/${seeder.labels[0].id}`) // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -250,7 +250,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/${seeder.labels[0].id}`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -275,7 +275,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/nothing`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -301,7 +301,7 @@ describe('label api', () => { try { request(app) .delete(`/api/label/${seeder.labels[1].id}`) // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -326,7 +326,7 @@ describe('label api', () => { try { request(app) .delete(`/api/label/nothing`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -351,7 +351,7 @@ describe('label api', () => { try { request(app) .delete(`/api/label/${seeder.labels[1].id}`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { diff --git a/server/test/user.api.test.js b/server/test/user.api.test.js index 9fcfcf1c..26a6611c 100644 --- a/server/test/user.api.test.js +++ b/server/test/user.api.test.js @@ -25,7 +25,7 @@ describe('user api', () => { try { request(app) .get('/api/user/me') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .end((err, res) => { if (err) { throw err; From 7612714e02b45df1da523c6df558e6af7b14fdc2 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Fri, 27 Nov 2020 15:48:12 +0900 Subject: [PATCH 002/281] =?UTF-8?q?[DEPLOY]=20CHORE:=20deploy=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package.json | 2 +- server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/package.json b/client/package.json index 11c35958..ef58ba68 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.1.0", + "version": "0.0.1", "private": true, "scripts": { "serve": "vue-cli-service serve", diff --git a/server/package.json b/server/package.json index cd63e7c6..2b8a8feb 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "halgoraedo", - "version": "0.0.0", + "version": "0.0.1", "private": true, "scripts": { "start": "node ./src/app.js", From f4bf9fca5c28317313ecd265d617da902a6cffab Mon Sep 17 00:00:00 2001 From: woongs Date: Sat, 28 Nov 2020 01:00:10 +0900 Subject: [PATCH 003/281] =?UTF-8?q?[iOS]=20feat:=20TaskViewModel=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 순서 재배치를 고려해서 position을 추가했습니다. --- .../Scenes/TaskList/TaskListModels.swift | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift index 77a87de7..c9bf7e44 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift @@ -5,7 +5,7 @@ // Created by woong on 2020/11/23. // -import Foundation +import UIKit enum TaskListModels { struct Request { @@ -16,7 +16,49 @@ enum TaskListModels { } - struct ViewModel { - + struct TaskViewModel: Hashable { + var id: UUID + var title: String + var isCompleted: Bool + var tintColor: UIColor + var position: Int + var parentPosition: Int? + var subItems: [TaskViewModel] + + init(id: UUID, + title: String, + isCompleted: Bool = false, + tintColor: UIColor, + position: Int, + parentPosition: Int?, + subItems: [TaskViewModel]) { + self.id = id + self.title = title + self.isCompleted = isCompleted + self.tintColor = tintColor + self.position = position + self.parentPosition = parentPosition + self.subItems = subItems + } + + init(task: Task, position: Int, parentPosition: Int?) { + self.id = task.identifier + self.title = task.title + self.isCompleted = task.isCompleted + self.tintColor = task.priority.color + self.position = position + self.parentPosition = parentPosition + self.subItems = task.subTasks.enumerated().compactMap { (idx, task) in + TaskViewModel(task: task, position: idx, parentPosition: position) + } + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } } } From 6a0d0ca29ce2327c423d647d2ee174ef0afbaae0 Mon Sep 17 00:00:00 2001 From: woongs Date: Sat, 28 Nov 2020 01:00:37 +0900 Subject: [PATCH 004/281] =?UTF-8?q?[iOS]=20fix:=20cell=20=EB=A7=88?= =?UTF-8?q?=EC=A7=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Scenes/TaskList/Views/TaskContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift index d924a2a4..45477c0c 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift @@ -99,7 +99,7 @@ private extension TaskContentView { stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 10), - stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: -10), stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), completeButtonHeight, completeButton.widthAnchor.constraint(equalToConstant: 30), From 6a36307e7d7f6316b82ce397c650d4e1e59e1c0d Mon Sep 17 00:00:00 2001 From: woongs Date: Sat, 28 Nov 2020 01:08:46 +0900 Subject: [PATCH 005/281] =?UTF-8?q?[iOS]=20feat:=20Priority=EC=97=90=20col?= =?UTF-8?q?or=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iOS/HalgoraeDO/Sources/Models/Priority.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/iOS/HalgoraeDO/Sources/Models/Priority.swift b/iOS/HalgoraeDO/Sources/Models/Priority.swift index 22152295..9cd694e6 100644 --- a/iOS/HalgoraeDO/Sources/Models/Priority.swift +++ b/iOS/HalgoraeDO/Sources/Models/Priority.swift @@ -21,6 +21,15 @@ enum Priority: Int, CaseIterable { case .four: return "우선순위 4" } } + + var color: UIColor { + switch self { + case .one: return .red + case .two: return .blue + case .three: return .orange + case .four: return .black + } + } } // MARK: - For PopoverViewModel @@ -35,10 +44,10 @@ extension Priority { var viewModel: ViewModel { let image = UIImage(systemName: "flag.fill")?.scaled(to: .init(width: 30, height: 30)) switch self { - case .one: return ViewModel(title: title, tintColor: .red, image: image) - case .two: return ViewModel(title: title, tintColor: .blue, image: image) - case .three: return ViewModel(title: title, tintColor: .orange, image: image) - case .four: return ViewModel(title: title, tintColor: .black, image: image) + case .one: return ViewModel(title: title, tintColor: color, image: image) + case .two: return ViewModel(title: title, tintColor: color, image: image) + case .three: return ViewModel(title: title, tintColor: color, image: image) + case .four: return ViewModel(title: title, tintColor: color, image: image) } } } From b3eb5b633e277ff020af227ec25ae6b45c30de67 Mon Sep 17 00:00:00 2001 From: woongs Date: Sat, 28 Nov 2020 01:11:31 +0900 Subject: [PATCH 006/281] =?UTF-8?q?[iOS]=20fix:=20Task=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이제 뎁스는 필요없어서 제거했습니다 --- iOS/HalgoraeDO/Sources/Models/Task.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/iOS/HalgoraeDO/Sources/Models/Task.swift b/iOS/HalgoraeDO/Sources/Models/Task.swift index 8cd27cba..b4fc53c6 100644 --- a/iOS/HalgoraeDO/Sources/Models/Task.swift +++ b/iOS/HalgoraeDO/Sources/Models/Task.swift @@ -15,22 +15,22 @@ class Task { var section: String var title: String var isCompleted: Bool - var depth: Int + var priority: Priority weak var parent: Task? private(set) var subTasks: [Task] init(section: String = "", title: String, isCompleted: Bool = false, - depth: Int = 0, + priority: Priority = .four, parent: Task? = nil, subTasks: [Task] = []) { self.section = section self.title = title self.isCompleted = isCompleted - self.depth = depth self.parent = parent + self.priority = priority self.subTasks = subTasks self.subTasks.forEach { $0.parent = self } } @@ -42,13 +42,11 @@ class Task { func insert(_ task: Task, at index: Int) { assert(!(0.. Date: Sat, 28 Nov 2020 01:14:39 +0900 Subject: [PATCH 007/281] =?UTF-8?q?[iOS]=20feat:=20TaskViewModel=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - detail은 어떻게 정보를 넘겨줘야할지 아직 정해지지 않아서 수정하지 않았습니다 --- .../TaskList/TaskBoardViewController.swift | 18 +++++++------ .../TaskList/TaskListDisplayLogic.swift | 2 +- .../Scenes/TaskList/TaskListInteractor.swift | 10 +++---- .../Scenes/TaskList/TaskListPresenter.swift | 7 +++-- .../TaskList/TaskListViewController.swift | 26 +++++++++++-------- .../Scenes/TaskList/TaskListWorker.swift | 6 ++--- .../Views/TaskCollectionViewListCell.swift | 12 ++++----- 7 files changed, 45 insertions(+), 36 deletions(-) diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift index 319d4760..9979e96e 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift @@ -9,11 +9,13 @@ import UIKit class TaskBoardViewController: UIViewController { + typealias TaskVM = TaskListModels.TaskViewModel + // MARK: - Properties private var interactor: TaskListBusinessLogic? private var router: (TaskListRoutingLogic & TaskListDataPassing)? - private var dataSource: UICollectionViewDiffableDataSource! = nil + private var dataSource: UICollectionViewDiffableDataSource! = nil private var lineView: UIView = UIView() private var startIndex: IndexPath? private var startPoint: CGPoint? @@ -127,7 +129,7 @@ extension TaskBoardViewController: TaskListDisplayLogic { } - func display(tasks: [Task]) { + func display(tasks: [TaskVM]) { let snapShot = snapshot(taskItems: tasks) dataSource.apply(snapShot, animatingDifferences: false) } @@ -174,9 +176,9 @@ private extension TaskBoardViewController { private extension TaskBoardViewController { private func configureDataSource() { - let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, indexPath, taskItem) in + let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, indexPath, taskItem) in - cell.task = taskItem + cell.taskViewModel = taskItem cell.finishHandler = { [weak self] task in guard let self = self, let task = task @@ -203,19 +205,19 @@ private extension TaskBoardViewController { cell.backgroundConfiguration = background } - self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskBoardCollectionView, cellProvider: { (collectionview, indexPath, task) -> UICollectionViewCell? in + self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskBoardCollectionView, cellProvider: { (collectionview, indexPath, task) -> UICollectionViewCell? in return collectionview.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: task) }) } - private func snapshot(taskItems: [Task]) -> NSDiffableDataSourceSnapshot { + private func snapshot(taskItems: [TaskVM]) -> NSDiffableDataSourceSnapshot { - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot() for i in 0..<2 { snapshot.appendSections(["\(i)"]) // TODO: 뷰 테스트를 위한 Task배열을 바로 만들어 넣어주는데 이 배열을 taskItems로 변경하기 - snapshot.appendItems([Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: [])], toSection: "\(i)") +// snapshot.appendItems([TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: [])], toSection: "\(i)") } return snapshot diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift index a629f227..3726f30a 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift @@ -8,7 +8,7 @@ import Foundation protocol TaskListDisplayLogic { - func display(tasks: [Task]) + func display(tasks: [TaskListModels.TaskViewModel]) func displayDetail(of task: Task) func set(editingMode: Bool) func display(numberOfSelectedTasks count: Int) diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift index 4630b77f..141ca1ac 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift @@ -10,8 +10,8 @@ import Foundation protocol TaskListBusinessLogic { func fetchTasks() func change(editingMode: Bool, animated: Bool) - func select(task: Task) - func deSelect(task: Task) + func select(task: TaskListModels.TaskViewModel) + func deSelect(task: TaskListModels.TaskViewModel) } protocol TaskListDataStore { @@ -39,16 +39,16 @@ extension TaskListInteractor: TaskListBusinessLogic { presenter.set(editingMode: editingMode) } - func select(task: Task) { + func select(task: TaskListModels.TaskViewModel) { guard !worker.isEditingMode else { worker.append(selected: task) presenter.present(numberOfSelectedTasks: worker.selectedTasks.count) return } - presenter.presentDetail(of: task) + // presenter.presentDetail(of: task) } - func deSelect(task: Task) { + func deSelect(task: TaskListModels.TaskViewModel) { guard worker.isEditingMode else { return } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift index f816ce3f..567f32b4 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift @@ -24,7 +24,10 @@ class TaskListPresenter { extension TaskListPresenter: TaskListPresentLogic { func present(tasks: [Task]) { - viewController.display(tasks: tasks) + let taskViewModels = tasks.enumerated().map { (idx, task) in + TaskListModels.TaskViewModel(task: task, position: idx, parentPosition: nil) + } + viewController.display(tasks: taskViewModels) } func set(editingMode: Bool) { @@ -32,7 +35,7 @@ extension TaskListPresenter: TaskListPresentLogic { } func presentDetail(of task: Task) { - viewController.displayDetail(of: task) + } func present(numberOfSelectedTasks count: Int) { diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift index a37d1942..28705d98 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift @@ -9,11 +9,13 @@ import UIKit class TaskListViewController: UIViewController { + typealias TaskVM = TaskListModels.TaskViewModel + // MARK: - Properties private var interactor: TaskListBusinessLogic? private var router: (TaskListRoutingLogic & TaskListDataPassing)? - private var dataSource: UICollectionViewDiffableDataSource! = nil + private var dataSource: UICollectionViewDiffableDataSource! = nil // MARK: - View Life Cycle @@ -97,7 +99,8 @@ class TaskListViewController: UIViewController { // MARK: - TaskList Display Logic extension TaskListViewController: TaskListDisplayLogic { - func display(tasks: [Task]) { + + func display(tasks: [TaskVM]) { let snapShot = snapshot(taskItems: tasks) let sectionTitle = "" dataSource.apply(snapShot, to: sectionTitle, animatingDifferences: true) @@ -156,9 +159,9 @@ private extension TaskListViewController { private extension TaskListViewController { func configureDataSource() { - let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, _: IndexPath, taskItem) in + let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, _: IndexPath, taskItem) in - cell.task = taskItem + cell.taskViewModel = taskItem cell.finishHandler = { [weak self] task in guard let self = self, let task = task @@ -175,22 +178,23 @@ private extension TaskListViewController { } let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .automatic) - cell.accessories = taskItem.subTasks.isEmpty ? [] : [.outlineDisclosure(options: disclosureOptions)] + + cell.accessories = taskItem.subItems.isEmpty ? [] : [.outlineDisclosure(options: disclosureOptions)] } - self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskListCollectionView, cellProvider: { (collectionView, indexPath, task) -> UICollectionViewCell? in + self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskListCollectionView, cellProvider: { (collectionView, indexPath, task) -> UICollectionViewCell? in return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: task) }) } - func snapshot(taskItems: [Task]) -> NSDiffableDataSourceSectionSnapshot { - var snapshot = NSDiffableDataSourceSectionSnapshot() + func snapshot(taskItems: [TaskVM]) -> NSDiffableDataSourceSectionSnapshot { + var snapshot = NSDiffableDataSourceSectionSnapshot() - func addItems(_ taskItems: [Task], to parent: Task?) { + func addItems(_ taskItems: [TaskVM], to parent: TaskVM?) { snapshot.append(taskItems, to: parent) - for taskItem in taskItems where !taskItem.subTasks.isEmpty { - addItems(taskItem.subTasks, to: taskItem) + for taskItem in taskItems where !taskItem.subItems.isEmpty { + addItems(taskItem.subItems, to: taskItem) } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift index 3e45372e..7cf7cf2d 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift @@ -8,7 +8,7 @@ import Foundation class TaskListWorker { - private(set) var selectedTasks = Set() + private(set) var selectedTasks = Set() var isEditingMode = false { didSet { guard isEditingMode else { @@ -30,11 +30,11 @@ class TaskListWorker { ] } - func append(selected task: Task) { + func append(selected task: TaskListModels.TaskViewModel) { selectedTasks.insert(task) } - func remove(selected task: Task) { + func remove(selected task: TaskListModels.TaskViewModel) { selectedTasks.remove(task) } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift index 6af9195c..6c64c497 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift @@ -9,23 +9,23 @@ import UIKit class TaskCollectionViewListCell: UICollectionViewListCell { - weak var task: Task? - var finishHandler: ((Task?) -> Void)? + var taskViewModel: TaskListModels.TaskViewModel? + var finishHandler: ((TaskListModels.TaskViewModel?) -> Void)? override func updateConfiguration(using state: UICellConfigurationState) { backgroundConfiguration?.backgroundColor = (state.isSelected || state.isHighlighted) ? .lightGray : .white var taskContentConfiguration = TaskContentConfiguration().updated(for: state) - taskContentConfiguration.title = task?.title - taskContentConfiguration.isCompleted = task?.isCompleted + taskContentConfiguration.title = taskViewModel?.title + taskContentConfiguration.isCompleted = taskViewModel?.isCompleted contentConfiguration = taskContentConfiguration if let taskContentView = contentView as? TaskContentView { taskContentView.completeHandler = { [weak self] isCompleted in - self?.task?.isCompleted = isCompleted - self?.finishHandler?(self?.task) + self?.taskViewModel?.isCompleted = isCompleted + self?.finishHandler?(self?.taskViewModel) } } } From 7094ec2c09afc2758a5de9108584d5bfda6c38d2 Mon Sep 17 00:00:00 2001 From: woongs Date: Sun, 29 Nov 2020 19:54:15 +0900 Subject: [PATCH 008/281] =?UTF-8?q?[iOS]=20fix:=20TaskListModels=20Usecase?= =?UTF-8?q?=20=EB=B3=84=EB=A1=9C=20=EC=9E=AC=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Usecase 별로 로직을 명확히 할 수 있도록 재정의 했습니다 --- .../Scenes/TaskList/TaskListModels.swift | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift index c9bf7e44..aeba46f3 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift @@ -8,22 +8,82 @@ import UIKit enum TaskListModels { - struct Request { + + // MARK: - Use cases + + enum FetchTasks { + struct Request { + var showCompleted: Bool + } + + struct Response { + var tasks: [Task] + } + + struct ViewModel { + var displayedTasks: [DisplayedTask] + } + } + + enum FinishTask { + struct Request { + var displayedTasks: [DisplayedTask] + } + + struct Response { + var task: Task + } + + struct ViewModel { + var displayedTask: DisplayedTask + } + } + + enum ReorderTask { + struct Request { + var displayedTask: DisplayedTask + } + + struct Response { + var task: Task + } + struct ViewModel { + var displayedTask: DisplayedTask + } } - struct Response { + enum CreateTask { + struct Request { + var taskFields: TaskFields + } + + struct Response { + var task: Task + } + + struct ViewModel { + var displayedTask: DisplayedTask + } + } +} + +// MARK: - Models + +extension TaskListModels { + struct TaskFields { + } - struct TaskViewModel: Hashable { + struct DisplayedTask: Hashable { var id: UUID var title: String var isCompleted: Bool var tintColor: UIColor var position: Int var parentPosition: Int? - var subItems: [TaskViewModel] + var subItems: [DisplayedTask] init(id: UUID, title: String, @@ -31,7 +91,7 @@ enum TaskListModels { tintColor: UIColor, position: Int, parentPosition: Int?, - subItems: [TaskViewModel]) { + subItems: [DisplayedTask]) { self.id = id self.title = title self.isCompleted = isCompleted @@ -49,7 +109,7 @@ enum TaskListModels { self.position = position self.parentPosition = parentPosition self.subItems = task.subTasks.enumerated().compactMap { (idx, task) in - TaskViewModel(task: task, position: idx, parentPosition: position) + DisplayedTask(task: task, position: idx, parentPosition: position) } } From 460e04a76763d42f4f1533a11cebf34d5013e45b Mon Sep 17 00:00:00 2001 From: woongs Date: Sun, 29 Nov 2020 19:55:56 +0900 Subject: [PATCH 009/281] =?UTF-8?q?[iOS]=20fix:=20Usecase=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=A0=ED=83=9D=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단순히 선택하고 말고는 Business 로직이 아니라고 판단했습니다. 선택은 뷰 단에서 하고 선택된 작업들을 가지고 무언가 할 때 요청하는 게 맞다고 판단해서 제거했습니다 --- .../TaskList/TaskListDisplayLogic.swift | 4 +-- .../Scenes/TaskList/TaskListInteractor.swift | 32 ++----------------- .../Scenes/TaskList/TaskListPresenter.swift | 20 +++--------- .../Scenes/TaskList/TaskListWorker.swift | 18 +---------- 4 files changed, 10 insertions(+), 64 deletions(-) diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift index 3726f30a..eca01f1c 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift @@ -8,8 +8,6 @@ import Foundation protocol TaskListDisplayLogic { - func display(tasks: [TaskListModels.TaskViewModel]) + func displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel) func displayDetail(of task: Task) - func set(editingMode: Bool) - func display(numberOfSelectedTasks count: Int) } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift index 141ca1ac..2ae70ffd 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift @@ -8,10 +8,7 @@ import Foundation protocol TaskListBusinessLogic { - func fetchTasks() - func change(editingMode: Bool, animated: Bool) - func select(task: TaskListModels.TaskViewModel) - func deSelect(task: TaskListModels.TaskViewModel) + func fetchTasks(request: TaskListModels.FetchTasks.Request) } protocol TaskListDataStore { @@ -29,31 +26,8 @@ class TaskListInteractor: TaskListDataStore { } extension TaskListInteractor: TaskListBusinessLogic { - func fetchTasks() { + func fetchTasks(request: TaskListModels.FetchTasks.Request) { let tasks = worker.getTasks() - presenter.present(tasks: tasks) - } - - func change(editingMode: Bool, animated: Bool) { - worker.isEditingMode = editingMode - presenter.set(editingMode: editingMode) - } - - func select(task: TaskListModels.TaskViewModel) { - guard !worker.isEditingMode else { - worker.append(selected: task) - presenter.present(numberOfSelectedTasks: worker.selectedTasks.count) - return - } - // presenter.presentDetail(of: task) - } - - func deSelect(task: TaskListModels.TaskViewModel) { - guard worker.isEditingMode else { - return - } - - worker.remove(selected: task) - presenter.present(numberOfSelectedTasks: worker.selectedTasks.count) + presenter.presentFetchTasks(response: TaskListModels.FetchTasks.Response(tasks: tasks)) } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift index 567f32b4..77ad5df8 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift @@ -8,10 +8,8 @@ import Foundation protocol TaskListPresentLogic { - func present(tasks: [Task]) - func set(editingMode: Bool) + func presentFetchTasks(response: TaskListModels.FetchTasks.Response) func presentDetail(of task: Task) - func present(numberOfSelectedTasks count: Int) } class TaskListPresenter { @@ -23,22 +21,14 @@ class TaskListPresenter { } extension TaskListPresenter: TaskListPresentLogic { - func present(tasks: [Task]) { - let taskViewModels = tasks.enumerated().map { (idx, task) in - TaskListModels.TaskViewModel(task: task, position: idx, parentPosition: nil) + func presentFetchTasks(response: TaskListModels.FetchTasks.Response) { + let taskViewModels = response.tasks.enumerated().map { (idx, task) in + TaskListModels.DisplayedTask(task: task, position: idx, parentPosition: nil) } - viewController.display(tasks: taskViewModels) - } - - func set(editingMode: Bool) { - viewController.set(editingMode: editingMode) + viewController.displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel(displayedTasks: taskViewModels)) } func presentDetail(of task: Task) { } - - func present(numberOfSelectedTasks count: Int) { - viewController.display(numberOfSelectedTasks: count) - } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift index 7cf7cf2d..1017f31b 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift @@ -8,15 +8,7 @@ import Foundation class TaskListWorker { - private(set) var selectedTasks = Set() - var isEditingMode = false { - didSet { - guard isEditingMode else { - selectedTasks.removeAll() - return - } - } - } + var tasks = [Task]() func getTasks() -> [Task] { return [ @@ -29,12 +21,4 @@ class TaskListWorker { Task(title: "두 말하면 섭함"), ] } - - func append(selected task: TaskListModels.TaskViewModel) { - selectedTasks.insert(task) - } - - func remove(selected task: TaskListModels.TaskViewModel) { - selectedTasks.remove(task) - } } From 5e4c3aae830eea38991e5484b776398dbf2e9d3f Mon Sep 17 00:00:00 2001 From: woongs Date: Sun, 29 Nov 2020 19:56:55 +0900 Subject: [PATCH 010/281] =?UTF-8?q?[iOS]=20fix:=20=EB=B7=B0=EC=BB=A8?= =?UTF-8?q?=EC=97=90=20=EC=84=A0=ED=83=9D=20=EB=A1=9C=EC=A7=81=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TaskList/TaskBoardViewController.swift | 20 ++--- .../TaskList/TaskListViewController.swift | 84 +++++++++++-------- .../Views/TaskCollectionViewListCell.swift | 4 +- 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift index 9979e96e..79b114ac 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskBoardViewController.swift @@ -9,7 +9,7 @@ import UIKit class TaskBoardViewController: UIViewController { - typealias TaskVM = TaskListModels.TaskViewModel + typealias TaskVM = TaskListModels.DisplayedTask // MARK: - Properties @@ -37,7 +37,7 @@ class TaskBoardViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - interactor?.fetchTasks() + interactor?.fetchTasks(request: .init(showCompleted: false)) } // MARK: - Initialize @@ -117,22 +117,14 @@ class TaskBoardViewController: UIViewController { // MARK: - TaskList Display Logic extension TaskBoardViewController: TaskListDisplayLogic { - func displayDetail(of task: Task) { - - } - - func set(editingMode: Bool) { - + func displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel) { + let snapShot = snapshot(taskItems: viewModel.displayedTasks) + dataSource.apply(snapShot, animatingDifferences: false) } - func display(numberOfSelectedTasks count: Int) { + func displayDetail(of task: Task) { } - - func display(tasks: [TaskVM]) { - let snapShot = snapshot(taskItems: tasks) - dataSource.apply(snapShot, animatingDifferences: false) - } } // MARK: - Configure CollectionView Layout diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift index 28705d98..808674d0 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift @@ -9,13 +9,21 @@ import UIKit class TaskListViewController: UIViewController { - typealias TaskVM = TaskListModels.TaskViewModel + typealias TaskVM = TaskListModels.DisplayedTask // MARK: - Properties + /// 임시 property + private var projectTitle = "할고래DO" private var interactor: TaskListBusinessLogic? private var router: (TaskListRoutingLogic & TaskListDataPassing)? private var dataSource: UICollectionViewDiffableDataSource! = nil + private(set) var selectedTasks = Set() { + didSet { + guard isEditing else { return } + title = "\(selectedTasks.count) 개 선택됨" + } + } // MARK: - View Life Cycle @@ -28,7 +36,7 @@ class TaskListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - interactor?.fetchTasks() + interactor?.fetchTasks(request: .init(showCompleted: false)) } // MARK: - Views @@ -42,7 +50,8 @@ class TaskListViewController: UIViewController { private func configureLogic() { let presenter = TaskListPresenter(viewController: self) - let interactor = TaskListInteractor(presenter: presenter, worker: TaskListWorker()) + let interactor = TaskListInteractor(presenter: presenter, + worker: TaskListWorker()) self.interactor = interactor } @@ -51,7 +60,18 @@ class TaskListViewController: UIViewController { override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) - interactor?.change(editingMode: editing, animated: animated) + set(editingMode: editing) + } + + func set(editingMode: Bool) { + if !editingMode { + selectedTasks.removeAll() + } + title = editingMode ? "\(selectedTasks.count) 개 선택됨" : projectTitle + taskListCollectionView.isEditing = editingMode + moreButton.title = editingMode ? "취소" : "More" + addButton.isHidden = editingMode + editToolBar.isHidden = !editingMode } // MARK: IBActions @@ -99,29 +119,15 @@ class TaskListViewController: UIViewController { // MARK: - TaskList Display Logic extension TaskListViewController: TaskListDisplayLogic { - - func display(tasks: [TaskVM]) { - let snapShot = snapshot(taskItems: tasks) - let sectionTitle = "" - dataSource.apply(snapShot, to: sectionTitle, animatingDifferences: true) + + func displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel) { + let snapShot = snapshot(taskItems: viewModel.displayedTasks) + dataSource.apply(snapShot, to: projectTitle, animatingDifferences: true) } func displayDetail(of task: Task) { } - - func set(editingMode: Bool) { - taskListCollectionView.isEditing = editingMode - title = editingMode ? "0개 선택됨" : "할고래DO" - moreButton.title = editingMode ? "취소" : "More" - addButton.isHidden = editingMode - editToolBar.isHidden = !editingMode - } - - func display(numberOfSelectedTasks count: Int) { - guard isEditing else { return } - title = "\(count) 개 선택됨" - } } // MARK: - Configure CollectionView Layout @@ -137,15 +143,14 @@ private extension TaskListViewController { var listConfiguration = UICollectionLayoutListConfiguration(appearance: .plain) listConfiguration.leadingSwipeActionsConfigurationProvider = { indexPath in let editAction = UIContextualAction(style: .normal, title: "Edit") { [weak self] (action, view, completion) in - if !(self?.isEditing ?? true) { - self?.setEditing(true, animated: true) + guard let self = self else { return } + if !self.isEditing { + self.setEditing(true, animated: true) } - if let task = self?.dataSource.snapshot().itemIdentifiers[indexPath.item] { - self?.interactor?.select(task: task) - } - - self?.taskListCollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .init()) + let taskVM = self.dataSource.snapshot().itemIdentifiers[indexPath.item] + self.selectedTasks.insert(taskVM) + self.taskListCollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .init()) } return UISwipeActionsConfiguration(actions: [editAction]) } @@ -169,6 +174,8 @@ private extension TaskListViewController { return } + + var currentSnapshot = self.dataSource.snapshot() if task.isCompleted { currentSnapshot.deleteItems([task]) @@ -207,16 +214,21 @@ private extension TaskListViewController { extension TaskListViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let task = dataSource.snapshot().itemIdentifiers[indexPath.item] - interactor?.select(task: task) - - if !isEditing { - collectionView.deselectItem(at: indexPath, animated: true) + let taskVM = dataSource.snapshot().itemIdentifiers[indexPath.item] + guard !isEditing else { + selectedTasks.insert(taskVM) + return } + + collectionView.deselectItem(at: indexPath, animated: true) + // TODO: request show detail task } func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - let task = dataSource.snapshot().itemIdentifiers[indexPath.item] - interactor?.deSelect(task: task) + let taskVM = dataSource.snapshot().itemIdentifiers[indexPath.item] + guard !isEditing else { + selectedTasks.remove(taskVM) + return + } } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift index 6c64c497..1b513a1a 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift @@ -9,8 +9,8 @@ import UIKit class TaskCollectionViewListCell: UICollectionViewListCell { - var taskViewModel: TaskListModels.TaskViewModel? - var finishHandler: ((TaskListModels.TaskViewModel?) -> Void)? + var taskViewModel: TaskListModels.DisplayedTask? + var finishHandler: ((TaskListModels.DisplayedTask?) -> Void)? override func updateConfiguration(using state: UICellConfigurationState) { From a16db5675245dc1b68f44fcea6dcc39c83e059e9 Mon Sep 17 00:00:00 2001 From: woongs Date: Sun, 29 Nov 2020 19:57:24 +0900 Subject: [PATCH 011/281] =?UTF-8?q?[iOS]=20fix:=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Mocks/TaskListDisplaySpy.swift | 3 +- .../TaskListInteractorTests.swift | 69 ------------------- iOS/TaskListTests/TaskListWorkerTests.swift | 56 --------------- 3 files changed, 2 insertions(+), 126 deletions(-) diff --git a/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift b/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift index c06ffbc8..c3b6085f 100644 --- a/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift +++ b/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift @@ -8,12 +8,13 @@ import Foundation class TaskListDisplaySpy: TaskListDisplayLogic { + var displayTasks = false var displayDetail = false var setEditingMode = false var displayNumberOfSelectedTasks = false - func display(tasks: [Task]) { + func display(tasks: [TaskListModels.TaskViewModel]) { displayTasks = true } diff --git a/iOS/TaskListTests/TaskListInteractorTests.swift b/iOS/TaskListTests/TaskListInteractorTests.swift index d9b0f9ca..776cd0e1 100644 --- a/iOS/TaskListTests/TaskListInteractorTests.swift +++ b/iOS/TaskListTests/TaskListInteractorTests.swift @@ -19,73 +19,4 @@ class TaskListInteractorTests: XCTestCase { worker = TaskListWorker() interactor = TaskListInteractor(presenter: presenter, worker: worker) } - - func test_change_editingMode_true() { - // When - interactor.change(editingMode: true, animated: true) - - // Then - XCTAssertEqual(worker.isEditingMode, true) - XCTAssertEqual(presenter.setEditingMode, true) - } - - func test_change_editingMode_false() { - // When - interactor.change(editingMode: false, animated: true) - - // Then - XCTAssertEqual(worker.isEditingMode, false) - XCTAssertEqual(presenter.setEditingMode, true) - } - - func test_select_oneTask_editingMode() { - // When - interactor.change(editingMode: true, animated: true) - interactor.select(task: Task(title: "1")) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 1) - XCTAssertEqual(presenter.presentNumberOfSelectedTasks, 1) - } - - func test_select_task_notEditingMode() { - // When - interactor.change(editingMode: false, animated: true) - let task = Task(title: "1") - interactor.select(task: task) - - // Then - XCTAssertNotNil(presenter.presentDetail_task) - XCTAssertEqual(task, presenter.presentDetail_task) - } - - func test_deSelect_hasThreeTask_editingMode() { - // Given - interactor.change(editingMode: true, animated: true) - let t3 = Task(title: "3") - worker.append(selected: Task(title: "1")) - worker.append(selected: Task(title: "2")) - worker.append(selected: t3) - - // When - interactor.deSelect(task: t3) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 2) - XCTAssertEqual(presenter.presentNumberOfSelectedTasks, 2) - } - - func test_deSelect_hasThreeTask_not_editingMode_success() { - // Given - interactor.change(editingMode: false, animated: true) - let t1 = Task(title: "1") - worker.append(selected: t1) - - // When - interactor.deSelect(task: t1) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 1) - XCTAssertEqual(presenter.presentNumberOfSelectedTasks, -1) - } } diff --git a/iOS/TaskListTests/TaskListWorkerTests.swift b/iOS/TaskListTests/TaskListWorkerTests.swift index b4bdc9b7..e072d8a8 100644 --- a/iOS/TaskListTests/TaskListWorkerTests.swift +++ b/iOS/TaskListTests/TaskListWorkerTests.swift @@ -9,60 +9,4 @@ import XCTest class TaskListWorkerTests: XCTestCase { - func test_numberOfSelectedTasks_init() { - // When - let worker = TaskListWorker() - - // Then - XCTAssertEqual(worker.selectedTasks.count, 0) - } - - func test_editingMode_false_init() { - // When - let worker = TaskListWorker() - - // Then - XCTAssertEqual(worker.isEditingMode, false) - } - - func test_append_editingMode_on() { - // Given - let worker = TaskListWorker() - worker.isEditingMode = true - - // When - worker.append(selected: Task(title: "test")) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 1) - } - - func test_remove_onEditingMode() { - // Given - let worker = TaskListWorker() - worker.isEditingMode = true - let task = Task(title: "test") - - // When - worker.append(selected: task) - worker.remove(selected: task) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 0) - } - - func test_selectedTasks_empty_turnOffEditingMode_success() { - // Given - let worker = TaskListWorker() - worker.isEditingMode = true - worker.append(selected: Task(title: "1")) - worker.append(selected: Task(title: "1")) - worker.append(selected: Task(title: "1")) - - // When - worker.isEditingMode = false - - // Then - XCTAssertEqual(worker.selectedTasks.isEmpty, true) - } } From fd50ee385858f61f5b4c7622954c459b9a7398a4 Mon Sep 17 00:00:00 2001 From: woongs Date: Sun, 29 Nov 2020 19:59:05 +0900 Subject: [PATCH 012/281] =?UTF-8?q?[iOS]=20fix:=20Priority=20PopoverViewMo?= =?UTF-8?q?delType=20=EB=B6=80=EB=B6=84=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Priority에서 UIKit이 필요한 부분만 별도로 분리했습니다 --- iOS/HalgoraeDO.xcodeproj/project.pbxproj | 15 +++++-- .../UserInterfaceState.xcuserstate | Bin 149867 -> 174642 bytes .../Priority+PopoverViewModelType.swift | 37 ++++++++++++++++++ iOS/HalgoraeDO/Sources/Models/Priority.swift | 31 +-------------- 4 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 iOS/HalgoraeDO/Sources/Models/Priority+PopoverViewModelType.swift diff --git a/iOS/HalgoraeDO.xcodeproj/project.pbxproj b/iOS/HalgoraeDO.xcodeproj/project.pbxproj index 072ae67a..640d4690 100644 --- a/iOS/HalgoraeDO.xcodeproj/project.pbxproj +++ b/iOS/HalgoraeDO.xcodeproj/project.pbxproj @@ -20,6 +20,10 @@ 4C3F5700256CF8A7006D7C9F /* TaskContentConfigure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F56FF256CF8A7006D7C9F /* TaskContentConfigure.swift */; }; 4C3F5705256CF8B9006D7C9F /* TaskContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F5704256CF8B9006D7C9F /* TaskContentView.swift */; }; 4C3F573F256D2263006D7C9F /* RoundButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F573E256D2263006D7C9F /* RoundButton.swift */; }; + 4C42361B257162C90068A252 /* TaskListModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F567D256B8CB3006D7C9F /* TaskListModels.swift */; }; + 4C42361E257162F10068A252 /* Priority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCD3256256F8BA700A46D10 /* Priority.swift */; }; + 4C4236AD2573B4640068A252 /* PopoverViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCD324D256F84BD00A46D10 /* PopoverViewModelType.swift */; }; + 4C4236B12573B4C90068A252 /* Priority+PopoverViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4236B02573B4C90068A252 /* Priority+PopoverViewModelType.swift */; }; 4CCD31E8256EBC0C00A46D10 /* TaskListWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0CA5F0256E9AF100E66045 /* TaskListWorkerTests.swift */; }; 4CCD31E9256EBC0C00A46D10 /* TaskListInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0CA60C256EAA7300E66045 /* TaskListInteractorTests.swift */; }; 4CCD31EA256EBC0C00A46D10 /* TaskListPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0CA629256EABF700E66045 /* TaskListPresenterTests.swift */; }; @@ -71,10 +75,11 @@ 4C3F56FF256CF8A7006D7C9F /* TaskContentConfigure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskContentConfigure.swift; sourceTree = ""; }; 4C3F5704256CF8B9006D7C9F /* TaskContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskContentView.swift; sourceTree = ""; }; 4C3F573E256D2263006D7C9F /* RoundButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundButton.swift; sourceTree = ""; }; + 4C4236A425739BFC0068A252 /* TaskListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = TaskListTests.xctest; path = "/Users/os/woong/iOS/Boostcamp2020/group_final/Project04-C-Whale/iOS/build/Debug-iphoneos/TaskListTests.xctest"; sourceTree = ""; }; + 4C4236B02573B4C90068A252 /* Priority+PopoverViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Priority+PopoverViewModelType.swift"; sourceTree = ""; }; 4CCD31DD256EBC0000A46D10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4CCD31F8256EBC8000A46D10 /* TaskListDisplaySpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListDisplaySpy.swift; sourceTree = ""; }; 4CCD31FE256EBCEF00A46D10 /* TaskListPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListPresenterSpy.swift; sourceTree = ""; }; - 4CCD3242256F83BF00A46D10 /* TaskListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TaskListTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4CCD3247256F847800A46D10 /* PopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverViewController.swift; sourceTree = ""; }; 4CCD324D256F84BD00A46D10 /* PopoverViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverViewModelType.swift; sourceTree = ""; }; 4CCD3256256F8BA700A46D10 /* Priority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Priority.swift; sourceTree = ""; }; @@ -157,6 +162,7 @@ children = ( 4C3F5694256BA228006D7C9F /* Task.swift */, 4CCD3256256F8BA700A46D10 /* Priority.swift */, + 4C4236B02573B4C90068A252 /* Priority+PopoverViewModelType.swift */, ); path = Models; sourceTree = ""; @@ -221,7 +227,6 @@ AAEE20DC2564E4A900668FAD /* HalgoraeDO */, 4C3F5657256B80B4006D7C9F /* HalgoraeDO.app */, 4CCD31DA256EBC0000A46D10 /* TaskListTests */, - 4CCD3242256F83BF00A46D10 /* TaskListTests.xctest */, ); sourceTree = ""; }; @@ -292,7 +297,7 @@ ); name = TaskListTests; productName = TaskListTests; - productReference = 4CCD3242256F83BF00A46D10 /* TaskListTests.xctest */; + productReference = 4C4236A425739BFC0068A252 /* TaskListTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; AAEE20D92564E4A900668FAD /* HalgoraeDO */ = { @@ -374,6 +379,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C42361B257162C90068A252 /* TaskListModels.swift in Sources */, 4CCD3205256EBD3A00A46D10 /* Task.swift in Sources */, 4CCD320E256EBD4600A46D10 /* TaskListInteractor.swift in Sources */, 4CCD3208256EBD3E00A46D10 /* TaskListWorker.swift in Sources */, @@ -382,8 +388,10 @@ 4CCD31FF256EBCEF00A46D10 /* TaskListPresenterSpy.swift in Sources */, 4CCD31E9256EBC0C00A46D10 /* TaskListInteractorTests.swift in Sources */, 4CCD31EA256EBC0C00A46D10 /* TaskListPresenterTests.swift in Sources */, + 4C4236AD2573B4640068A252 /* PopoverViewModelType.swift in Sources */, 4CCD31E8256EBC0C00A46D10 /* TaskListWorkerTests.swift in Sources */, 4CCD31F9256EBC8000A46D10 /* TaskListDisplaySpy.swift in Sources */, + 4C42361E257162F10068A252 /* Priority.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -403,6 +411,7 @@ AA354C2A2570002600072657 /* TaskAddViewController.swift in Sources */, 4CCD32772570A4EF00A46D10 /* MenuViewController.swift in Sources */, 4C0CA636256EAC5200E66045 /* TaskListDisplayLogic.swift in Sources */, + 4C4236B12573B4C90068A252 /* Priority+PopoverViewModelType.swift in Sources */, AAEE20E02564E4A900668FAD /* SceneDelegate.swift in Sources */, 4C3F5683256B8F4B006D7C9F /* TaskListRouter.swift in Sources */, 4C3F566A256B82F2006D7C9F /* TaskBoardViewController.swift in Sources */, diff --git a/iOS/HalgoraeDO.xcodeproj/project.xcworkspace/xcuserdata/os.xcuserdatad/UserInterfaceState.xcuserstate b/iOS/HalgoraeDO.xcodeproj/project.xcworkspace/xcuserdata/os.xcuserdatad/UserInterfaceState.xcuserstate index 4c7eaba272307e71116bb06ccabaab2e447431a1..d2669b1d926199e8d49fca1bdbe5fec551d912c8 100644 GIT binary patch literal 174642 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 literal 149867 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

8e=%=-EZL~HM-}-wCzM=JX?HL`7?>)@H_tV{h z?-6@ncV4f-ck+zGH>a$}w_BVt$PJwgIry0IyA4kpjv4+k+VKggK74%T4r9IXcazmL z(Bw0%Fzv*r;QVEFnFpC?;)4ZVFn?-MSh`t8Tjt;mZ?9W^uJ{WghAG9B^|KKn<2BKAT4Vs$HIowPTx6M7oUF{CK-*^A*>FgQnx!JSR^J#QMbZT@_^qT1X(dRojI}Gozu*0?vCpvN+ zQ#%%Syuag{9e?lCxzqSg%R9Z$>D$hx&N-duciz_dlP-~626UO#rMAn5UFoi=U1xTE zxa*;AvTn)Uio31v_Ca@9_g>vgxlF`DV|53Rb8EH$> z_NM*WyI1e3-cR@bsZUIwnSCDb^KIYGeSLj5^*!Cs)6d&4*zZ(-SO1&(Z|Hw&fO~*< zK<$7p2SyJp9JpoRH|br|XQXdWKbH}g;m>$3%o#W5nYrq@<#P|t>o)K9dEd;>p1*m4 zVnM-z{R`a-Vc{2x(ia66OBYXDynjjblBG+|)C{h93?g7Aymxcl&8u$y^_KCs>{)7G zTC?=@t%Gmfwk&Fyf7$WneU?9Zo8q>T+upxD@%9IA|93^ zqxVPLUw!|n2ZlYc>%k5W-t*w4b;awBuFqJ%^C8Yl{+hW;r$CiJ#mTx`v*qFy&t4pb?d))T; z-P>f_s<)lpK6(40C$gT{wIgOnaHnDC9ZyQ0tbX#lr*3-c*waIw-v3PMGdrH``0PW^ zX`fs1y!82b&!2yx_=QuuChj`C`-a{7_w?EG+>5agzj5Hr^fzCAtKVCD-|qeP?srn( zdEwn&?>>Jp`QUT!CBOIF`@P&P1)=6v|>(Gf=v z9~*n@qvMm0pZciqqi;Vh{rKk-vrqhevIdp-6`!g;UHh5sv)a$Qe!lZmuTw95k@dyF zFLS>7dP{r&rjKg@qT_Gjv!Z~is@ zuW$aIbBVe1z&~C8+4Jwve?R%J?7#oe)-!K@GYd}bg1lzmF~p+9qJ^<`5wS1T;0`mI zc^YhOg@SFf_GUCNUFAGlY}679uCKfDnnjLem}ImhS290If2)Fc*5You#YgZM=HyY!OuUztQElX0j$M9EY#txPX7%FMD9 z*=Sj=Y^-dO%qJ_CEs!miEtjp7t(QG2+bY{FJ1jdU`$2XdH3(MTUhb49$W!F~<>~Ta z@;td$UM{~yz6^DUmGZmf_sAcVKO)~G-;UbElk%tWWp>f}IDE(4K>Z;7aQ!I#SUq{6 z82?Wu75++Y3H=TI(c@0jhxNFEsC079h$#(r!Ys66Z^TpWBmuO@k#8XbyED?Mlf-Uu z)H{_?hx9fJA^Ut7vfk-Nd%njRjrhe@;o0c){Vx>m zEV%zaU{4B-)zR5WXcu3tv!jG;tL0S`qM6NG+>7 z5$HLmqN03y&ytb3Ju@mQ0#yaxvdVycD8Da& z7GvCRA!QWH!ux|b$2ccRVz)YToq5i&&T-E1&IwL*)%zCMv%sRe2Rfwx0PK&zp4;lY z(K%VV**VpjOSWx+{RtwL;YMx5atuzkYzfuXjLvJe5Q*DKD&;lT92OgpE4k1SXc@ z^^f-k#+3xf4)6Gqs^VPSwsx?L4qQ z1N#fG7l6gRxQoEzM%wSd{!u6C=$zx6>zwDD?_A(qh>g6sCJNX;f!hMyR^WC5_atyn zV`FpAkych=`ErXZ=8W*pE}1TDJ(5!mU(@|Qtfv}JjjckLv#Jsw?td3np?e_-mQx<6 z1ZTVsWDtt57v z^A6`q=bg^GoOe6#ao+1( z1I`DX>zwPI4>=!pKH_}TxxpC(jsi{zoEf+tz{LR9AGlG#ksAg8Zs#`y$*rjN z7x+RWcWh3Rwj;rD$f4Z>i4uEwr}KH751mgspK?C!e8%~#^Eu#X;27Xo;5gtSfNQsf z+*X8G?{U89+{=4oBg*Z8iv%u;be0Rd2|K2tIWsD%vP())VDZBicZ2g)(U%1o<10Xs zc6lf=a+@Z^cS^=QJ_W6BZl$*z_jwz)6tP{&Jlr%!NW5V3RgIbJ3Q&f5D+A5q3bWGd z!W4y0BnuYA^>D>4>fu_DkdV^Dm6DvaXps;G{TIkICbrl?=Z8e(zUO@3`GND0^RTnt zc?38WaBAQ*z-fWg0jCEJ-`%uX(vkc)KbCcLp2UWWB0s)jt2c1`oMF<5V;SCtNI3sAOWVTyUaG;wI;>&Wq09oWBES2hIVU6FAo< z=bz5MoPRqn0p|wJ16(w49Y_|2N?~HfF_BwPQR&MpM%{sIVcW1hss?!j^YSWk%2EE} z+%uY#hHs}E`GOJGnTm4M+xc&~D3NjdTkyJ6NpK4q+aSYRRZvVMP97Q@`S`d^R$h3_ zMY|Xm>*8E!KXkQowU@pnnc-5pR1>}E8HkQ6<q1iKRFBckE{n@L5f#S(?=l8l=e#0xS>)A5m)+&y9Xvui=mHfR@w8JC zXzfs`Hv$$ehv96vj+?97FxDs7Su4Le1f$Ire9B}c#B>ThLD;t6?SYX8SLKbklS2~NeOeZr=_ICX|TmWf&{r{p9tj^X2j%%%&s}z z3BjAqdH?IA-pWZ^XxeM(AVORPdd>bG{bca3pPki>3w<+}1*V_oB1AdWrSqOE;6mL$=t6mz6Lif6?gmnC zo46(VxuTJjfQm+H z!lFfDrEYQE+E}S&z>Ta~Azr%OwW8TAG_E^c>m;$;U3a)^-w!)JFRpx`IU5(&n8E% zEg;7@Q^=>ByzZKOpx%O(4{{9LR8l*#K56J2>~o^!5|J)4BJ?u$E1qu_mP|W%&WK?} zHG%hH5*)tp3h^@?*+Q>>)gXbDD=0yu1uL-WalH+>rYWgJK&B=+4-Mrg{ zUb#(cdb;R`{aMOXqVOj#* z4B%#Nb{!*{vFl^k3E)ZvwHU|cB2qI#5npl zl($;wRANz!$M{3Mvwec@R$4kwOeb^HSU&!A(oV(bb?f(`X3JaQh@!w>y8_=(=41hr zS?$LdRn6|<7~?DQ;mHCYE>Z*pzHBG0b)9y71ssZ-a*z*4O;eO1%}qJD?z{R0SI3U7 zcprBdVYuxP`#Y0uFEgif@=}8-_G#lh{FXeFfD-3m#NHyTo5n zPQ1j0e1ww!fj2C;{&fB2`rC!7cK|q)sB?jvhgLn;e{P9H>6XfJ-113{IgjPb1YA|k zUf}Sax~_4qtG7>Vz*jZ4lGu=VInkvnuNb;%H-qvTJ4*0977Ry3q2|*a=$Y zwire%T?8o|aC7*=Ovo!H%(>!2S6!!tt7ZIEF^X0=IX_hXZs?nbxBa+qZAHe13-5Bc zMuqxom;5LE6KZ01TLhz6vnFq>7JBRpYZv~OoLaY))QT98;E$+_lE`p7~jV7I?z}+EQ309H; zP3CcN4D!QC&>#3!?aY$ON*^wf(6V`@xF@4B8|Thz1|UK|x1iWp=DR#{NDjFZ-N_9z zs=F6(w>HeE?ld>Px;@wqXFCYGQ3YSta7U|qfO`;;UW?r6?hJRPI}2xY;Fbe-8*sM+ zw*s$-Kl~NE82V4DYZRfq%!=HC9MFnUh`#C*482ptbA01mr<=yO@hN~IZODzY{hf`~ zoZv?U`<`zE*7jZ@Fvz}*Afy}+&73F*^ zYIH;yXqIkn1)4!LJ;-im0pICyjFEnYWh@Bw4pn%sX$VSanyj1}8)ln4zv>H^D zPYHxfSRkrSHJ3Awx&zj=wR#VEW98z?=n3ZeDiB-K*|<@Zk-c zwr<<`%nL8ReBh0D-aGtZJ;QR5d6g)~C!+PhH+KkH9ydm`Yu`kFZE}dRKDWA(+{cb$ z&xP&m3#cUZ2Qp{AR*`zlZO-<5nayVXCb_K@e zq8ZN@sJB>ck>O@AHDNg>?{L>To8G!|&B38!>Z3b!{2x-k2UG9dt+q?oCgp8DmheUd zyi4PMNOA)v85g-RzH360G{+7nno)gHa<8yc@}6;HN~wJ|rS-m6%c^Ac{rV3GKgGI4 zOffyOHY2l1(V86A56T`KermT(!PIghgEury0dLU>KZV85U<#x9)Q%q0BnjMw5QSM2 z0au?lc3jvAPfa+22~UXJJdr@zB<%@e%Jq|{Obt6Z?`s?;chj_5Z_{$BHo zJ0@akb2Xdh^)}9LlH53c$rPwxxM*?sX+3-f)2f-a33@lHWc@8mZw)_@_rAtNmanM2 z?e-?cx%O14qUu-Pc~{uUPfavn^7m*q-P^8t8CS2lD-6Mhao1{U?|-04(I((-aUoj2 z0`=?GKlDG8!+~i%61nNocFo3jeXw?8808QI@YKZ4n8@Zn8@IGGHYrj4V|9;*T@dfO zM=*`;k(-`qXWY>wk+FktLw8>NlTSSzc5!AT{((t7JFWJ)=bI#S?XIiewR=z4<(!&0 z5EI|KulA*;bAd`x|H`ZT|A$0|U?Q(YZh4)EhNc6t{>``E4!@$ABQTkFBR3tq45Ifx zI23jwQxnO^tM5~Lr0MiifArY#u&d+!bO`~$*Z0NvCAB9$sXcizT6B7;{q(cwf#n1F zuh9coxSt(hIUdNjyPf89*UYEQh`4sP9 zxhVNvatVEm+e?k;U)%}(i~CE5OUI#a&{Xscx>>qZdb{)?={D)J=nu44`lj@#^f>wj z{e)gY40;5qWLB9QJ&M!tNll|=<779=iqM}pAe$?@L$*$~QTCW@8}Cv4hU{b6x3ZsQ zzsdfP%h4yuD(@ywM4#e5@@)BJ^a!d#kD!(Ed*%0|SMfIV13DnDm!Cwx;?weT^7HbG z3Ks8maVa_~x}h(5AM_W`RZLUNR8%Ue6iXDhEACPR6;CR5EA}Z4DBf2bRh&@#sJKK? zR0O4@ER>TOLEtt5x2c7dC0R5O-Iv_18`>un_NuNhG52V?yugAAJIuu1l;OkW zdgHRPiRszB#=Vv-IdDm|A?UszxL^yWXZL#d!?^r%V@+yml%(G z(M=7*?v3uvjai~w$>zrOU7h=J!7v;>uyP;|huEMy-1y|kpnE59j|JUN0ar(w%#D^Z zKiZ+tY~}YAX5y*^9|?gbGPD|!l|W8bfH&W`(UQ5@{k;1H_b&Ht_a5LL2W}g1+ktxm zxE*MW!ae;4!w}i6YVb=DlZ+)jkgw_$3Pl{4iqNfyzun#Ezj~Ag-$+DM+~f!=BDqUC zUzPH|R_d(#E0q)saVoc#pD42l0A%S7&dEzS+EVAenFkwu?m5>!T z+h0-Eu;ni%{-OJ{BzA}UsQZ}vxcejb$L$cvH)2A@XL$rF$d-{WpE&h} zno4j;608ZX(3k4csuH|iCl`0MtMEw=|MkVcrJr9pK*G=+Q|!dJMoFB%2uT;iG9V*~Up=GWpi% zrl-QMr2mF{r=tT#b)jz})>>$g=z+_HXJ3egc6dBj%PtzT>j>NjBs>0H8f0&xrR-n9hg!%BS-N^d{%{Z4Th-`PtVK>*;x8lZ&;U zI7y)CiAf_XedRgj=;bK7TzL{bNrJ#`V$b)acv4*#JiR@AfIA7?ao+xZ47d~ds-6vE zwwZBPFtx`v4EKiJdZBmvcL9`=g3Um!kbr`9uq=o44FBot&6ZeVyu1NT`ILh?Kl zai+)HfyQ~pdvHtgbKp(^_XTiYZt_g>+~}E1=L2^dA_hT3B}jkaDHM8N@FNY4HLY-N z2#zzH!tf;8V{Rmb4n<2DnkAczei4>JPcc3j#pCl7d2m1KE8xxm_w`0k3GeC6ebeaR ztsy;@9tfq)V^0J^$e@Q8c}8%@Icpwz=9CnOXM{@6lJ42;@p}TEDo?csjgsF2_Zvh= zAp&nV>rw+BdFFZMdlq;WN|c^Op2eOeo*EBGW_WJ#EcM(fdDpWX_h$-24|8kQ=|rKy zDZB`0R^kmyl&z4X&gJ4m8Q@)J8=i526Hz(uZ;BcssmA+Ebl>h-LFks{0QWO+xSh2SxMMYYg~qyDx@V*3 z9?!j=RluP$*pI;d)Cg@&)BytR54h{e?+MvObI)bQ_0WF2u(RY@C&W4@#A?5M&W#Im7>=oRYCcX04 zhrWdnSgodN*x#E-Qj*giWY?|YU*1SQZLA>--&ujYo!=>6DpB1CTl2N78Bru2SJ=so56!t*6W zC?JA{2z;Tb$k-t|AKM8tkp9Bc+u%&ny#HH*S4`?0-{cYOD=nm1b=)@P@F}hQ9=jblcU8B22caQE79Rm>th%iEg2_no8VSxxM zMA#t04iS#J=$=FwmvoFy6yDeoodOX~UN?_|2scQFqgw{vM$wT>QkKTCF31Ahs3uQH zLTwQ@ov*xz>gDi~a@1ifQK(<_awclTnHBf|C+~C={LZU}$-(`1{-z2P5m%;yKA)%@ zH%M&^6MbbW{I(T8tHxFo72)ps)lw2Rw}eo{!f>rBrB_#>A1h{bWsS+0&&DS;p#gxG zEz$T+v|w}=M7TnaL5%)C_U<}N%evbe{yDT4NOwrg1=0+|Ffa^5Nr;4WcS(qJOAIX_ ztzaMsVqs$|AQA>5DIp;eA|Voz63=Jg##{Hj_kQDe|9qe0`P|2`h4VVscdhke#G^RVnHu88;>|;$Q#cJArQhb6;e`NIx$2#mI*fLxo<9Y@8U1O#0kAcg^mP4(@!X z<##^1{VVU?>zYS;n)TAi7JiadDC{R$rD&C`aAHJ#Pf(rK;qG5;cRt#?dqs!H&bNP9 zLu4mU(+?H#RO%G1eBrih{_7P!3i#KTdKDe)+ldBMD)t`-FtTT%#K_)}eF{yC>>KP$ zI)Ei_AHe9uP_f$wu;#dK|K$JPsyYYX_CH}BKO@B_}3pjP5;+3_`jdN>32?F)cuK_ z>6;(MMsyj_-BAupWz5e4g-M!QIk=yQ^zpY8)%Uq>G1k@Tfp|M&zV>BHj0P@Tk3zyCPCo?7ruVzq`%J^1kc|>U{)BAuQaE`yML1>H zhbB!EL(LLH%@aeNlMc+Ck6!P7v~{N!{&X1HtKX18|N0Q@&VRi9y#^gKmi)&@UjO|4 z&2B&4_5bp>-~9yY?gz`oK5o`*Qs=Pla6nnqfG1_&%SVhVz8;hJEbOCNb1DG1P9E zM`RQT7fcMbPYiWP40Rk=u*IFAE$;OE?ib9L`;RXDt-1eGOaA$((VYerOKMQ@#89Wd zPJOsk*auF&AgAEnO3Q}hd=peSI$SOs6D}W)4Oa;JQ@cxIsB2=VTVkktVyH)A=#j)w z&sXyA4abKohZDk8!d265N_zJ`)GIO6J2BKJG32=U0?GMFyLjh24E{5NH<7(@eEVyR za>hLHK+SSxYsN;GkF8bXfhhm4x}V2T)|ar|{iTZk`Ah#=tN&<@4@A2@>id1VC4H_t z$PJ=P|Luj!|K|(cy+?)r{G}p&{)dfg`me7C@-;i|aJP96h3oyZn+?Mc-}w)B2GAsJ z#EW4^_0ij3Jenct{ZWfp;Le9_ci%<|w+#DQ{=d(pZMfatSMiojxMSG!ER(*L_RhBa z{auo!Vb7BLpMEjvtLXpr9!keMm)@IIdu;r1uz;-p9LhufN_!{OgU&Hg`T+OL~X5_3d{elYaBv z+YJu;5oj-khlGcQhlPhHhK41E{O^dw&`3XU-4AIleEU7}&wU7Y`(5Qcw;u@Y?G0}u zb{}^8t??$Ehu!&Z03ZJR^H=YF(^}H^eObPL{NnA)eFfFOAML4E+5hodZohicyDxWZ zofw|%ozw87#L(j}hJAE*`wN->hj&iH)4g*Vo)LaBF*GJIl=$!W#lo}0^Z%Q-GQ&>= zg~JOHLu0*_85%ck+FxCbtTt}y-~YiU{CwCCU;U4FUc>$^BD^dyl=Lz~5zycwTk)Tpv03zUkEOZtAv zvgM-UN*1VCp@MIiOnQ`X_?`TzAI_BG(|>;LT&mPX{@*@A_@|5+(%j=q-tIgUI7`-~ zuS?72OV09mlyK6sdkcH;!{5H|++z=JKYd#1zKQ(KFZendA0Txf=&8U-Zy_fA1B2gI zH|gAa5JL01b!+HXf@4_;{CgMV*A+`Z>tZ_U=NdFQru2KK*wpQQH+yAAhL+dH*29MZQ-_krUg#w8z@ zD$PCPlHKE(!YRguGI16LtIlXrbOp<;r>g=)e0*=+5^x{8j6nl6+@s_3{ry zRgbP+*3T`8^*D0h=U=LNlpkmkRjzzYc|QQBX7%XkJIDI&-{ndF2zBRa0Xxcgidwmt z7Iz-++iIA%WBU!OTT=!7|N3W?dkS);|LzgNzL~FK(|`Y~ zO7pt49!vUc(!ZPh>kIIg#>M~j*x(%rRmxPYp7em+|N1QG7psj+`(h2P&9b9b?J{*9 ztm`iiTs1ww=e>{QD8fOWVeGh#Kz4RK#}fTDC`p=q^$J@pziv9!)bYnD5bcuXwQC zN505pmq(3Wj7S@i-Ite?j)?O3x|oRa5wRX(7Z(v9kq}WeqDDl`h}sbkM%0Vw>Pt?h zM$C$MIpVX3!yZ`|kt|KJOv#ESE1s-mveL=QC##gKR<#dA-|S83#boC3ItTbY2y&#u>*nw}IpoRV+8mweLN|KQliobaWB5{|90M7Q zry%B#HOCyhc8;f5z_To33CoZ>$7{TS+&SJu?i}ZXAgAyD$f<`pr}G7;v^pCPGlNwe z4}#o!kh>TqDUBKAE=PH4AaibW$lZ!Im`83ks86 zPf9YAANR`RUU{O3CWcrlQVadcQ@jNhBzee%0ce)lO*96M0J>h#1*b_9lb6Tkc#+`U|PQP)9n`mG1OJoyjbj3nc>?(g&0;S0x6neK;{{&iC0^lm z-ry5HV-;&Wz}yZ*Hp3jfu^2?^ZR9!R3(Mzo@gOW;*z1QE<2A$1M0k1NaS)WGG(>**~4~yo*y^0nj zl0~>rQF)5K#XCVztSHV>G5b?2j(EQ22X?cUeI5|r0sSrBmwxDR@uSFG`~nxb90Voo zPzg1ZP(um(Q$mkRnr+D(t#$3xh%k!9RRBrC47$qphYkbJZe9GtksIO0J z+R~nmoZ<@Cu*+r5I@&%(&%+KyFW{LVD3=~N%gI?z|I6urx!Ej2=5jKZlQ||#S!z<7 z2YCqhjxqa~Ml`{m#n`i$Ui85XV$2}M3}OZ`gkg+e6r+(j<`WKZD+tP)fBCw&w)_lM z@ID{m_m%&Q^=xJf+t`6UD8HM%?8EDo|AjN?dHFwrAT|O$kJa97`2AA%;#NJ<31HP@D< z%g9b;>{=CeB#X~ellu}f0D2r?fvL(otAX`E_@+HWZV3rAbozMhX6XZ%Tp9E(q!F>|^9g#4DnapM`=9Tag zZ}KT?Sc~0D_?mC|j_;8n;UqV><%c{(kermH#+j;;78$B!pb%lyU!^F;DM@Lfh$eb7%%$2OhA@l~j6$AjV@br!sx84hs;RBI9#=2M!??D(eAVB@%&M=#@2hTZ z)xTv2@>So>UiNW-pZJ+GoZ|wQkhQw3HRP&cUNzjOMhQw0jTzN2qZ$>dgRC`LB2SGD zbfPMo6UdPVWc!!m2RhU*?U9RxMgk`;Ms<|Z$NDNR}Asab(aRHh2mXn@((Y>K(nY)xB+Gl^-~v6{1( z&(l1^bI4Ouo|>Ox7B$VH=6YnRDO1hw*@_v|Oser#5Y)2Ywam3v8q{1XJ(*B@Ew$HD zd#zGLVGgy_UCSJ5#SxGEwW?BsnsjCydR$9CYn}8%Su*0f+A`H1$SB5;h`!a9r}k4U z9(aEb5p=opPwZ zj=iqa1$$JdCw+K~0SsmsBN@$X=ArI73owH^&+`J#Or51H$DY+OgF4R7gQ+P-0|w!` z2W5KDIeG9PKXa5{_?;{Ki5zt!NI@#hs&0rJm``2V>J}i9!pKrrmZYE7Q?~`J=|CsC zB41s3>dI4hB2O@#naEUErn+WP*DUJV>AGsHYlrH7#}=H4x@xYw2Q#Rv_PT1XtM-W!Oy3zn&cR&T)w={22spAJk7q zO3bl-cJ#Y`EY3px$~Xu0^}c>B9z?eKYOddyZm6|>FYHzQzVyd=sBaeahcW{7*I&p- zsHOgmAZSpKI=HUEG+yI9^s9k94L)TZ-(fxtwy~34$kgCRj&qXVa8?>z;G!Sql$rd5 zxu0T`rYz+sPfgsvp?zv-mm0RE19qvQ88qyP88lRX!^u2@v(V5PZn%`?yvhpR;$7b7 zE51eD4ZmkAJNN;0H&l1S1N?-2dN>by{BUO`;q@Mt_u(@^(8x|TGLuHR$cz1HBu}F# z%$y}ad0rEAHr;+TVoO46(!!k2BF&4;tIE#^%yk zFB*^IB{pKlO)}!TCQTX0D4dBViOAJt2C_7frOC6HRg=ZM$THqxB_Hw$pR<~M{K{{f zO~uT9VS(tET4BRQ*jKrVDncX;1p_7y}s0Fh(+(*~~-T zP1W7h44SID=?g4jDfX=CE0{skBSFwCHTJn#0|w!`W->MVfrI?aQGVffuJ9*vG>;$! zsW7YN*~m#A?xP@)RKx2xZ$dL#(vA*vrYmO9d{Ii@Ttv;y^}o6PH&=HHb+=G=3)x%TLptnOixND9{BWBdH8;{Tnb86WavuSBIE$v^+2{`*Lr(zB*-{T|fPs`6)%R206 z%dgpu`?oy8S!8K>8ChE1^7C)xXeCE0{colBtqM_|io{ce>eQqTvb9okt48Q~s~+^E zH;>W}b7(b$VW_>;W*HuAJRWtD^4q zwWz~G)W`0(pUNA2kNb4UiEBEvWhBl)hsivFOdaO4h$Sp#Ij`^7DpcbEWa-!p{q8uJVW_v`XvU%L zj_U5H?v68)<(e!~npU*Hl~gP@CgyJWyVb;*Jmbje9>@^T*qQGb_e z*zqoE>7tKazTx*E=<2$z(KMzF?dgO(UHf1@UCpQK2p(q);~38@=JFH^d5%SV%2#~H z7PhgAz3k%vr%Ag1^&sdLL2~R-x3t`g8FVv)ZtCw=n(EY~4iC|QMl_`bt!YO;2BPk6 zLmAE})ZHx+`McSzZq80OedxBHUxT1~2>0pU4A*pDz=y2iOEw@=_pQj$U5@T@bpMIN zm{s?4{DIx;Zl}86z&Yt*7Cr1v53k=NigHw>5(!krF7+^j9%j%(jvl>ujQ$KlO+7|3 znz778zk9sJyS&dwe99`mU>zGV>mK`2Ymc8f!ZGCTVYhmiMUOM6zsH3jcqAib(A!6b z@C<8kUC(5gNzdYxLY|)Xr)NCo)6;xDh=TbfgR2>50AS*_UbPbI<2_ffsq1 zS6RWEe9me%v6b!oz>mn(Q>LCW^*oDy_sU2XvXhIvSUCz;JWoQK}>^nQickfpaQz2C;Hddt&$ zEq1TBo$CE9&PwkST;fk|20@>Il%yd5uyR z3}pn5GnVm8Vk-8ik9zwoVJR=6_CBxk26n8Ey8EnTE58T9qgg0VYrNj0%lHzrc=RN{ zahCJQ(>DdFk)!XuWF#|L$xb9iC`L(S>sywFw51bW(eJ*!k*#k(1~85Z%-|Vh=_^ZL zHT8WJIr_fI+pNYe^*xAM`yS;uYVND%zH07!0dwek)z5_1^T#rf37H?uMow~Lw;sEX zf~fzoYIH;`k1gjLeh-3vuIpz`{TgE~{n{f>zb+VZzrhT{eEOMBzcI+v&y4!bVlH;7 z-$Kl)-y%N6?)3YP@7cyK>}S7y9N;ub_rD$l{UbaCUtm- z1~j56Eimi;?dZoq)ZKq5!x@FT`zIoQ|H;^`{`$~=J--IQfDrC8z_}XWngP!KfDc)N z-5Ia}nFefSFZ=n4!yLn$1~?-FPH}}lxfuilWgBR}2FfVHwQWw11nJ#*#^os zur|$UMQ6IwpCOpRz>$naj)4=AW1!v-T*?Q0%xA1-EwT+%^T2P}jM@ht;TL2cXs-sE z!$5Nwc%DDFgxwmb{y{lW=b*NX=0&{TAQ=bi*I?&ia5Uwqh&+QIpgs@NgyzUIxDDnr zxEGINPJ_*9@DOAhJRh$=_+?&W1@EvD*#>{aM!x0;e#8t0o5A3t$TV302Pf5bEeM9B zCO7#gfZh)&f|`evqzq~wQl1B?hn*Ut?jg>?5PLAhUJbEVL*yUQ0q0_fJ`8!DFEIO| zsqlJ38zIxsnJnO07O|LDc^6rR$}&`zp`Tz@L**H|3Fl;}b24-%ySNbeQKY0qmSOrm zECZRyN_Gl!KQY7+PZi8ySS{ol_7E}*>%j;f#|{k}&m^WY9d!?z!+c)hb>84D%wd?? zhkb~;hpBs*9UG~nOord!mLE2s202D#Asac!MIMS$ zg3^>lwh^&3r4!xhNpJLfgdG|&n4!osLY@(FjCg@%Eaz3;Cjate}IOn6jX9v63gPDvv#9^F` zQ5U!r1dk^t4R-1Ad&!JUkL&;AYJ0pW<*7sh)u_dT)Wi9BybbN>Kxg)&caNXqH_isZ zXjw*&LEWS6!suDdV9d5Pr#YEzf`JdD>K*O#G8U^er3 z8Z#gFJZ3)5%*VaWdwjshe8y_N;7c~M7c)=HO$>EtN;~v1Q6Ce#p@)f&GaBv(+}{|I_CK9Pyo z?eWui5}C)#Jbnqwc$wFD1DVI$kMS#!b^N#3>j`NoOhu}rhZFR0!dS*LiK$G-Jti!~ zYfrFm6J(gMk+1j~uQ_1{hxiS9JmCVDxXN{I1;IpnIWYs7$wp4{a32LIMH$MX_Y;Sp zmWlFCoQ(cXbgzlKF{g=kbK=1un3SKAltvFHl|!#4>D44VF-ew5vP}AhUF_jU^nS8? zPcDJ$C%gY-_n%xDbC_HOvzXifdp!AJ?AzoHbfgpZVRAqEGk`&i!@Vb~eX`mo&tX0b zc$P&h#(XC0=j3--83a=b6OGwVG5aZIKcznIJEaNDX+>Ke#eJs?M4zS%XB1;_?@Wg!-M14=(PcdSt zNF~hYiP!j$kNK2UK`^Z(am3^IOf$b}`a3P@*Vbcar`e%to7stdm}Zxz?dKr3f?#?9 z%wT$b^nSYDPuGX(9q33G`Y;kbnf@%VV3yP0<$XS4HD9m}=ViKnOy7q4PT$Rs9N{R( zgJ4F0xy;am8G0}yH~G+m8AZ^88S>32O$90;;|v*R)a4->&;xroV<;ndoUy2N#!Sp^ zhS|+{ipxRpWD4B($&6$nJGm%?-FUJn&d8JQ`(zZIumexZ_vGmyn3)E@cc$Mn)9;yS zk7kymCJkvtHy)uked&+dX4=u2Pok!o^LUy?EMXZh^D1xiA!ar6LJ-V~AcVSSse4vt z@>2kNGb<9knpK~s4B`pA{w%vX%WKYB!CRQwEHj(+5ufr6-?4@5*xgxs*oPgNbtnjC zC+9xmkZbmM)IED6Utz|xT{n9duAP01Uohv{CpgbVuHZb*kzV&m&Od+|&p*u>&IQ3!YI$lb?*G&T zCI`XOsWH>1v!h2(>(SE-c!`&B->2Pofw?Zwmj!0IAc1PQ_k!=)%65JTf`!d7yM^7* zi-mfzP#p`+YT;G>*h}Ig zf1Z>3xhCx35QjO+@gR75J33$o7TbZv?!Q>S7hm8aSAt-P`!CV&B~vl;C1(Dj`@g8)FXkgZ zg;>TryvO@|6a-5vQj0p&r9PYak^LOx=O9?7@5}7mvH=WYDEj_V3R01VwCMXw&+r_J zkbAklFE33LaxX8BzAsIL0$62wwBruVo<{ImnIIe(hCW=MCNt zg4ezF>j_k)IyLdyuWx53yVx59E86e~c4tK&9^(vlXNCE#xD^C%Bx3@0=Z(3{XF(9W znUliYPccg3{%?N5XRP9jAb89D-)cxBn$R5gf9nXxIL^r+c-#Ho9?nSY&)fF)-6^== zyHB$aS>MZx`@L6!Qpoz=yR63j-un_+SJtK}?zgffvaa06uejgJ)BGL;@Aqdk?)QEo z6NBJ`lw>9=*~x`De(*AS{efP8@MaKvXpSGoqt_qm^@lYu#}Bun*B|a=cMyEkitgz3 zM|%Cyqx{A-u5&X8K90bgKc2-L<{|fIL-5}}Q`cvcc_Iit4`KeFtLyVT+{a?{|MRza zhm}FFDhB;u)eJpYr3b6p(}}M1Ks~F}v&ufNa-UU`(DzmDvuZjsnT^@6n$H6CV%3NI zz||mFt%lX|uasIU0>aWm;)jQb59)3peSL^-iGo0l-e+0oA`PVoZMntRDW zN#dwNH6GwW9-;vc(}B)B#vt@;ja^z}u4~M7ja+N=Z;hU=S%TiIQU97x_?$I-$p*e= z6W^oWHAne{Us3xSb6fK}>Ryvn_ht0pi|mxAHKQ<(FTCCtCxT#Y8tx~Ga>P=R>d3KH zjG9m$m7Q`+oTjyKvpQT*$Rfu5~r3 zLtW%r*A~0Ct|OhXf9tw45c67RUh8CAHwO9EE#h_F#`#|NKIXOVGgk8jTiDOf9KoE{ zox+UP*{yZwgJ8W3>(f(^FzR1loKl#_dh=Ls9_!<%j5(}V@A_u6pcQIgulDt7U$6G{ z-RQvtma>swgJ45CqH)~@nKsNvjtxs##&X``BV^ei%Z62~VJ&8~!HhPT(FQZxU`89{ z+i*1qHl`vivTV#qHgb}iyp%xiHzrV(8tDH<{oh!hhRCqd+%}Fzk2k7+<7A$|EH~Pr zjdOX5g{(lm8_js*dzisSwQu~C&oPIM>fX4HL);32uOg|1o&Cz|edVluwViX=k*{t9 zft3utPKG>RXC()@kmKw7C_o{~pr2otN4Bryk?-p^Ji?z$s-gZ(wW&*e9;OM+X+rKoIL5OW`?Be95PX+{qBO*H-^uiy9s5p>@9fogYp_?}eajAH`A(Ma z_Og!yoa8soa)C?8w>cYyaLzZ&vbiK>i6NGXI2W7sesgQu(gFS7tpA&P(i<5zo7?7R z(BsYO-@Ft%wAm~-ui!1-<$bnL#U9LIv${9`#PuNfJ`Yvt%p|+AUYv<7@@$Es9OaQ?OC>5}K3mLZi}`FZpDpIIr6uOE#ola@W6M;g zGn=_Q#R6XDH9q7sR`CTJkZFrdTV&dz?^}N5Pi_Xm)(FgTYidHMd22?pp!TiB(EF{W zQTNtp)V;L=al}&vd$zSbkF$g?`6URprNedGWZL!^!x+hE#v;!)d$VmGvTT!On^|pp z4)fWjpWEKTe72d-Hu<)h$F@TpLzZnP`JMCp!KEPBo|4SuK>xSv|Mr4JQiP(&u)QWN zX@mN=cft;B??Epfr5^(^-|gz%K8+d7MD5$vzFqCx)xP~%p63(n%l7L*u*2MU)SxG> z+ac2q^VlKBj$Q1d}x!G@&zkzjF{n7>@q$)c>91nTQNK&28sO^mwQGcYe+q%yOqa+W9q`_@19p?@lw` z`736yQ|&v|zEkZx)xOglc3ugBAF>fcE6o0f7g&$$cFDBMJa);kE1L3{%dTqFMV4I+ zc$mgC#f)~D(JnLEWk$QqXqSAurn8VmEM_UM@EULM7OT+vU0c}3PV|45{_i@-A!OKP zZo4y}$Gg?PJ12QC%iZ>9cbNMrP6F!PZN|H6Vg|d_zFY0P)xKNpyUk&DGX^l5_c8n3 z*MeYA0bI97rak7dM~*!+nS;6Pd7kCSvPYIZD|nN)F{3?Zw8xD0n9&|H+9ThdJoAd-Z>>{_m|sWn|cEZhQNn$9vVkcQC^+%f0q!?>Htf znP*V%UNhdi1T)yH_PuJ~tMQurC5x_N62>_mCDd+Gj@l%xIq(?K7i&^6h(oMl`1-ZRkj6y3vCn=>5JaJb`n% zPyhGn|GuYLhz$G8ZQobu@jmtM+sY2ia^GI|^Am@;NK(CKyze?@uwU)_Q;-UC*st#W z_fnj?^ko(+@p}7j1i^v4#A8Pe)ZroOBhP^jbfOEg9FXNePvki;oKcJ+5i>fVw+G(j zJwD(gK1aU~tYsbBk>S8me&GaXILig(Iw-@zOoX`~^&c!n6y=B|j>=TU{vTBDLH$42 znzpF@pxO_r{h-J(nDFfwk-A^+8v;aANTE@$~%Da5ZDrEWTOV+ay z^ZChqe$vmM_9NR*KXWq(4(b1)jASMoxzX=K`6-AzhZ3kkP4xee-XCg6W16D=L;V=f zB&IT*nb@I2^HKAmXIaEMtmFgq|IjCx!69`YQuiTsANmUU59$BU_Wb8!)Wdy#ehSz8 zyobv{a5x}2sjyRr&E>Ehhw~xF;X;HdLQ(Yca7FCY;VM+;0nFmC{X0AmXZ-Lm9>@M1 zPGkaeasR_FumV{QzlSV`_5ZLOhvhiDmaQD%EEl-MRjzX@2#zE}wj*jjVh%^{qacwK z#$FwM$N|_p*N!$$9N_(nI};9F?)4vHtIfR7RU7e*fuT(!7n*5*I&He zFOztQb$rJbwzCs?e))wHoI;LY%;}f&n9Xs$JZ?6}&E~k-9KV<1R6vg7RjE#G>Qave zw4)b~q4&r2{`hc4BGd7)sQ>tLyv4h`&qvsw*^!TLu zPi7%IcIaeY@>7VyR7AZe6R1jc)PAxKbuovN>OR?szRchq%>Cr0AUNgqPF15L&cmrG zOyfyrAN8femi3s?DZM>)7W;QfmQ&Zb6$GavNI@#HA;anW zDMm@kQjS>UIxWNLW;{Y~)PK4^gBZ#P9%n4$c?$KOewOEX0kxl2`)ReGR{QDKd4q4T zGry&yG))+e>wc5zw;z$?w_iBHDgNLFvYe6SOmb3^nylm?H~DbJ&&YSCD)nhhQ(DlL z_H?2P1JL_36PUzQ^#6?hpP9>iWH@7PXV#&|XVib@JM7RIvpn+yd)UW8?Ee|{o-yMy zS1^M!YCog)-_`!R+J85P-_uYSJM()_%>MVc*oo`T%5>H|&dPDN2DLDkvyEttEN5jo z+mX(6#f;9H(OEM(Yer|y=&XEapW{{DM3%Gf@e!Z$Ijh->-k<%6pE-*DpVj}fXE=)t z=gjR~e)Ra9`p*@m1ZH{89-WJ!0+nchde52hxn`KbIklft`#H6rQ~Nn{IM`{xFX}5P2iSEd9u{Zq~$Y6#t3BA9#fM;+{FY5nA z{lEA!uOh=mbGx`3J-(>^i-$OZSzfe97fL#L`N&Tp?xz?fDNO=;f9+u! z2xjblr@u%Xj?|1~Y-v9P|F3__=g4r~+^(NOkFTr$ z`X5}zEU()m|3A2qoK)mOy*KWo0A_GQ?Kg^29CNs#?i*!kNMGi#lHGW*n+2&xN9@SW zK0HQ$#XE94zjv3w5+nd|i#a{Mt2>rf!jN@Dif?G1&N<)Zy z$xK#qkP8`ZmFGd~q5fNqX+}%h(4J0o#s1$Kf&Skb%~;fa%M5N!;R)1zYbGoBmNP*_ zkdYYLFos2Z&IZ0_6Pww^PyEagj&Yn5T;eL%xfMi2BqId{DNQ-bQ;`I!QiGZ_r4!wG zgx>UH0D~FIB<8Z5S6RVZyvzH1#HXy{3wEI1h&}wse$*bJ_6W5{s6FCWP6iRlQd5M7 zc$C?Ei0hKclsqSLBoA{x#fT;zS(3|=ygCn1izYOu6>aH&e91>MjakfLKF{zRFR+BS z(fj0US;t27Ke_%V-^zAmNN#Q^BGBU$>Q52kUd%E@7P6Czyp%z`Da<%UEM|~G?J3lr zLhUKkp28eb)TS%rc^R`$aXN@dnHkrmlqscoq?9A&I3{2&DQB_(SyIZ9auJJp5i?3@ zMk&oGr5U9(qm=Tc+{-VV;y2E6k;`1;dJvH+Eqb3SANeVS{-@IaR3#~m45`d5ReSU} zmHJclpciJD${wW}$Pk7z9rdO%<5Y7ogH&oy^(^X6rS4Qqd5N_g3L;YLaq1$tPwM`- zCiP0TvyX%P%n|e}wH&Fh@FzEeh%^x-CnaW;#*EVFX&RZ*gz?&GWJ)7XnkdRq9y3W( zhh{uNZ~CH!G=mt*2p(rF=9cCu79vlY=do95USt`|d4<<`gKuyK?n#Gx-_ry=y2o|* ze8!K+bdNdRa{{xv=MQdhD~Jfm6_P6?S4gfz29WE(Q1JHj!{DV=kY?sv{3*S+e$HzSeQp?mGny(K9_ zG_COEroy1Fg&B-7leR^VWU3!_)KaCvem$Dpt zmELU9e}XLOSFwh*tYa%X*oCv7en0Z1|1*flkcNAaB|~QPJA)m{keB-?MJ1|I9dpW1 zmwGg$5i(?Wl(Fb>2K8r{iu0afCUcn20-ohf)SKZw-p33ws6B()GpId-+B29#hV}d$ zMEDRYB4e1^IENW$QjWLA4-wP#j)X0>NFhs*^?pdI5d`^+0T9YkbtT^5D@J7Bk9XMp?`#iy38+FUv~4U<0yb`IaqgXD7Qj zhTdnn%vG)j5n1&=tNv$AMH*ztYHnF8pvPI&pS2nfV3t|!QP%oAOcT1J-mGSv^-;_q ztJ<>;W+>*6Roz)f^DLimfLlRCwn)5Qw!X|@1$HFcM|{fX$dm1RwjxV5S+dEJZ4dHf z`;}9i;T&d^U2n7JqY$!Wzn_wnrYz;CfehK3(Sp`=pc7q@E4vKY6PeEf)SrD3OIXIs zyv7^6js4HA-t1rT4VzGV_HFFo2h^SYM=l2uIkHm;`;lWDUN48a8nWb;CATcO=ORz;r7Y)FR`4czn|n9= z`3dJd_b=#o?o<4RGm%GzJn6|u7IKoCe8`nYhCJ~!q%rEx(~>r{rxSK4PY-(WIO@$~ zm-0+t5^B$*_B?9OqxL*=n8!P8;X)9RHyd$u#C3UP%DWah@@{4;&P3jQ97UGAzjBh( zoZ(My1`+upNI`1k%U6VQR79413Dm$2<*P$oTG5uC*rR;?7|1Y2AXmOIJjt`XiTd-c z#17>%%Y2`+hA-K`*Qht2UCMWe!>B#qanzkp-TBm=&z|MegZs?jKD&NjQ-<&iUhlpm zK}7y!6r?z%h{9gwmnVNsYV#luQ6KZlFHimsbfz1RU`F{TGmnKl$0C+uhw{I|YkZ6h z`M>2m%qjm)cCi<^^2?C_au87<#J#A$Ko+u-i@fBg5QWkI0_rWG{{^a29kmxQg92ty zz*#P!?gEYI%M9MZ+zVU^A_{uFg4O7VSrnYYG@fJ@@)UfLWyn%cmV&YrbS4VQQ}8oZ zvzGO2L~je8~ z^*=n9`N$A9x9~djIIRBgci5q@S%!aL5BoTX{ST`*Y{ua$m_b z!V|p74qR6>JLRc@Y(-@&ss}|MrWI{yhkQlNsc0AaF_0k)XB6@keU?{vgSU8>5BZqS zScO>?-OE7^afDwv$#49Q48_uN9|ci=vHK}bDWWJxEOFFFy~P^Sl;)_tn7I^dPe;^U ztQ(0eVLiVD5yj2DxYsM*j6vA5;?MB{FR~1IihqC{#Xm)k;%iuo`4l&w;^tG_e2SY- z@%@-biB!l@;$AY6jU41AFD0l*0?tQ?8q~pFm5`}KL)2fwK9(5GI3_R|b1X4~S*W?h zQ!Hc!dSBul-s62fLfs`kXEo|Bu@3z&aVv-@8A&aA;q^+cWIN}$#*HAt+prO(>{Kav zN@c~~l*)-LrDQ2(R;A=ARhqKIPyzEP)q<`#6QyJ+)t7+`W*8%wiVUTm<#`tK5-;-_ za+Q*y)K~n-0n}gW2veEQyKeG zdMxH%+8Hc;Du^iKx-v4AX@DGMTGNgWJVJkDDI-gnp$umvQ<%n+%w`_)m3faf*uOFx z`G)V=$_{?uD0*M!5?A;$h=|hvDE*IerlV3LLzKBi#iGYi>W`|59f~r`s0XP>LmJZ! z^+uU-R3FSBO6^fnUU%9>GGGb(FFW#x-bPaX;&OLSq1Q;IT_r5bu4-GpYeME|4pKe{tr zks;dLqUWN=(dv(Wju$Y?XnPd>3a|4fU!dM-GmicWGl*7uwA!Q99r!o2;qyI6hSc42P<`#1XJ&sX-%w?`&mfqHnD4(2E+(TZ} zTi%SzM`8x$)m~oh<<(wZ?d8p(d^sA?k9nAV`Mul?kH8 zOROxh)0n|b%qZ53V$CSljAG3wR=(Jc>|hT+a)84eZiniFJ3hJ)VjYk;CXvUJr1k_$Z?G@BsLG2Z0F^9LN8n@i-T8dLCzw z;_A?XuJptV;>^I$8HtD+jJ=8*fedlZN8GEdK>cy=@;)E&DXVZk;?}VX^~UYv06(Gj zxMLj09OBd+cP5Cal$PQ&#Oy20$Lm!($c-Q(-k!zVt@xsppcL}NSEV`+AWOU~@pX|W zzBTRWNEgf~UT@=PVh7`&W+98v@A##>#7bm{-@sRV%NDk=6S?ALh(8}hR8C16)L%J0 znaD~Ga+8k<aCh^}li@Dx>zwW>C2%wNZEFdh}!pE3hAx&jk?)UN6Di653!E3FDc> zRHh+M!t*!}39=-}k|0aM%gB@PA)oL$YgmikCY<6dvLsyO8rQiMMEIE^5mhoGLzO~= zxu24hrYv$*k)evSQKbvrQGb;_JjMV9V~47YWHhr;Zxy>#WdYBi_9|+xqV_6kud3_8+nS~71%&ppL^thV(t9^wXs%Dnewz7j=?8W|9 zQ*Sjhu66-4sHXO6e{utJsIKnnDJV!ax-ps8@p{$&2qJ1^AsRbUqYBk|0C{ROrzNeC zrG_juIwDVvehg#?!!e^8dRyZqUPG1|Z?TdO_?S=m1{rGX;{b;^#&J#}*Z*nhzT>Md z&prU3A5N5FwN~mt(Ymz`s$#3SG7g!Lom47VON)w_Ktd+wBq4;Lf{P52kc5PjK_<0O z90do~R>dudqP{4O)?(eGV&BWRubkMd3^h;@&@QzVG>nOHj)dl&X9 z)?UT7(}}!d^%1*~%{;^w9^rA!Csv-Z`ib4c_vh*-Rxh#pFr(N{LqVK*#2v&@oW_}) zgZtwy#x12C6_@k~H333^GG$Vr@x86}uef*B>4QGyvI z=+EB`5+wYNo4JL2iYTUx3T{XC32n5q7WpU0KVg6&^pId~3A>STg8CEQx@9hmt3AF_&@~v4ly)E+wlyIURK;t2_Ay%pPmxv1I?O7?87cZp*~p*R#uMl@MXxFPOWA=QQ+A=x zlyBG{3j8*fAl03z2Q!v&9Kliim@_#CJLc~V2~sa&3U(}YDpwJMT2s}Us@7Dsrm8iy zjehJ=sy#{_gzY@X3%nc(W|+r}(^2P) zv-ugn;Z_Q;Co>lD0P>h2j~P$loHRYAorFD1I}K-~IV0^{rjbQ1*J4N0X5;L%+wgAE zR&Y1>(LfXH=w&^6PBZ7U4Q#>wrETL${>Ib1k1W!@!p^1HxpXs0AH#{rKK(popDz3K zOSqIc)R>+`Dt0Yh{^@otU5)8;n9EJfqZD<}Kc4FZ-|? zerr{bc>s1Zb1Z7f9M1$MaTe!de=;X?F;lpT1d^GKTr$lvGl$=DBlEB$nS~TnhVR47 zT2|1=YV2@k2VLkXbBImoDN|3GThUXdo-((ygSXI2rd`kcfDhTnm!Tjlgr2hWlr@%d zjAsIq_$jAz7UyyvzFV@c;A(uwWQ9o}2eZ!7N0yoT`$vMT<@o-~x(l<&vX@!Sm`PSU z=9{$%^=GL+%U))AS6PqaePr3otY`4XvR>dN-oTD#t3BI$%T`x*4BkfeEyyg}{$$_J z6YLBHIesojKRIV$HaS-kkGgY`F_)Zd@|cOe&AFcY7{(jPd7jsKlRfOkd&&6+^33sG za>sBGhjKVa@ne3%@l3?Mx#pbPK@S_ycdlA<|H{)m%PYK!J9FKc`#=1h5BY@ugo3<- z8Ou11;289pcLFDIGH2s^D9=01i$lM8`prwn9_HycZx&{kH;3OdpJFOQLB2PiZ|?b1 z@cDe7&(B~s^3Ko4_gelUN~z%c&)vaF?qL;8v|u**de7g6ehQApJ{5#fS3xe<;%yY% zjPna>sAUDd9}4VJK?7Yx>191bjIf!9_%rsYz#bJm#ddb^As_KEpYjD?^DX}g1^&$G zV8MYLf_xXqb%7Z!I1V#h;O+(PUa$@`T+km17M{ZS=w;#6#Gi=+kvodqQRI#ycNC3sKWZ-W zj*GnGqOG{M$h}3od5gDsmk*GC(f9X#!smR2_fzy;C|DHYMC7<=CbC&njkzv*4xcOj z0T*xs{(JFaBGj;ydhB2EO5{_#h7M#@to~y47puQ`5N8&D5eiC<;!Mn{#O{@tOUZl+ zScbhTxd*#k;+ztFm*~4h-zEAk(RayK^j)Iw68V}%<_*w-@qS~ih$xD@+WX8+3UUzr(}>8Z>eWqHW7 zY!<&kpJlQulTn#W%JfjS1$CFXv&?MDoL@ecA7L)#XK^m)qxSO2T#S3m6Sy9GU4API zu+!zmm{0j~?nI8|Ees;Va`%?Yto#k^Nx5C9a9_nwIGJnEOT}DnrwQ|`7{VMXoKdj} zbEt4ug*jB1LxnRd>E=<)({H5?D&jtu$z^3v(heA%AryYm3JYFO6OIYd8Ha6 z2XO*&j>tJ8=g4Ga9GQZQBW4*f%g9V-VU`iIjJPvWK_%6wHR8UA`yzd8WHS%31wBL_ zM@Erd$RhGxD5$beRR?kihcS+0IgaC*$l1gYkD98InNB)c-Csj?GQ>Z_7#RRmd7 z$)c*BJ6OTp_+G18h51&kri~8PqV_5oRqf&1P*6Pqxm2fe3;M2>XZ1SlL$y7q{xgrT zlf8Vx=X`}7tJZ6cUTgGPb0l`I<`{m0UTgGOGl^5UhM6ovtu^YZd5|Z0kynso&Fj30 zI&1#Pzi@Aj9jp10Z&3e|qq&@XoVR2XTiJ#)mpp}jm%JAWY7b&OYOM9STD{lWjoK^G zf319L7h~?V)zo6{wcbjtIoCR`_IX}n2fNsfI%_{fUA6l|f&YymSULvxExnLml7|^C zb^cO&wA9`%UCBMP(2KXYbdV7qMjuNbMK4SBzEswAGOn}Bbr&&Q3<2-?W>f}@R4DW=3We0Nx_Ho$^GRQ{l%VwhfWj8aQ0tzXn6nnVr zZtkOz)u?BgdY0*H*$|`L&n6x~P0P%C*+-$EURL$?rrvv~pMuQlr*akQsgENaZ=ybz zYf(@AY;M6m)vKevf=bM<{&rTNu6lLVtE;{l`PR#zem8b_xx1I^eYtnKd@QGOAs3;R z7u-Hm;!)^)>!26f_-xvzqczZ<9Kk9%d_#@dS3Q z>HD93AKx8K->^RvG>4FX^C8H-`EbCwFl#4VZKD zM*hTu{Fz6PU9){~CdJeuht>UT=Jim}G6q?THoyi+ozt)1sah^|b6l9WCbCvWIv0g#UzsR<*U- zp;j|(oy;$|9P@0w1~s>uYilZLWH5*M$gp)KYgxw-53mK9w929NZQR$om-qM<_qOS^ z?H~@ttlFj$kNeu(*XF)98MIx;Y;M4vZL(`KuQu~)Gq1Kguv2a8kwcrAv^~oUyv$DA z-L{X9`HU~IH){^yV8$|zBRG=NxQsCA%;I<4$UN-QnnjdSK_&KRO#^*Ai2Ycjjy397 zqmDJ|So0ch@Kz{j517c+$h5tHTJC0;4g8T!$h7@ow(=<3Fw=H3ZU6o(8MnWSH_@)g z_OJLi--Ut>_jKs9lLb?ubX;9rEs&&ab(NThL?20v03Z4mo$MM137S?BHK~ z9SS;~-T7nAC50L2sWXdQuEiWXujjYOty6BDa_g){-<`{FcISOG(o7V2cA9Oc-Rj(o z{yU#VuAOr2R7aA=w+??*XFa3B1$OdHX_u}L4wM(sCYVEp{ zySSGIno(<)8oLG=;SZ>@OPyU0@+4334A1cozT)5P4+Y&J)Y|NeAL*zh*Byrzi#vEHotE3>%JX%cFVK7i*?wQ?tawWE!*x|B2E)+x$LFQ4JN5^v{`if5Cr<{(yqWX&J zE2@`hF8YYh<~PVcD*xy_=3~#J_B?9GqjzHuqjHbRJ*uZ@2kMX7h3Fpk;=M)X9Novq ze8v}i9SVBX)uXPSqnW^Q$h+q>)YNk!7cqrjBKID-_arl&bZ$k?J#y};VhL*NQCm+V ztC4Sye0v5^U(W;lg}?GN+j$K&_NcK(jXhuT4fdkfUi8}IUc1pN(_S_9p2W$>vRA#m zXL1fd<9sHg_TDt?MX!2#&9iq`DCl!u->JkP=f0bnPXYF)Pd|ON*q6TBS%IGV?nTCZ zU6_AgAA0H=;wg5rn>Uev-+O$(hv={yQr&QUH$6n_x}3r?0UJZm-G7Z zOh88K&){s%ubIsa%wZLe^Jyp;Fsp&7_}KwJGvH?ie#cx&k@y*vG*o)KO0ZjWn^E z9%MBrtHFoZ!lP{CNz8OmW`nzVi?@;8pzH=^H~0}Bhk_yX44KB=%xhAH$zv_9N~d(Z`5fN8~!9j*&^6g<3{rJEF&t3;8)|%;#?U*@U}B z+%@8^5qFJj<5l)xo+E$fpP1{&{!lP#uA}BUdKl(9>i$vpj~>ejcoU;%Z~;?^B}^hI zWRgQ3Gr5kr6mT2nKWhH|4EA8u{6}x6oz0l@#z^S@{yA{We}6r7!+(GM|9=M?YyKC6 C+f{A= diff --git a/iOS/HalgoraeDO/Sources/Models/Priority+PopoverViewModelType.swift b/iOS/HalgoraeDO/Sources/Models/Priority+PopoverViewModelType.swift new file mode 100644 index 00000000..174a9618 --- /dev/null +++ b/iOS/HalgoraeDO/Sources/Models/Priority+PopoverViewModelType.swift @@ -0,0 +1,37 @@ +// +// Priority+ViewModelType.swift +// HalgoraeDO +// +// Created by woong on 2020/11/29. +// + +import UIKit + +// MARK: - For PopoverViewModel + +extension Priority { + struct ViewModel: PopoverViewModelType { + var title: String + var tintColor: UIColor? + var image: UIImage? + } + + var color: UIColor { + switch self { + case .one: return .red + case .two: return .blue + case .three: return .orange + case .four: return .black + } + } + + var viewModel: ViewModel { + let image = UIImage(systemName: "flag.fill")?.scaled(to: .init(width: 30, height: 30)) + switch self { + case .one: return ViewModel(title: title, tintColor: color, image: image) + case .two: return ViewModel(title: title, tintColor: color, image: image) + case .three: return ViewModel(title: title, tintColor: color, image: image) + case .four: return ViewModel(title: title, tintColor: color, image: image) + } + } +} diff --git a/iOS/HalgoraeDO/Sources/Models/Priority.swift b/iOS/HalgoraeDO/Sources/Models/Priority.swift index 9cd694e6..dfbe3488 100644 --- a/iOS/HalgoraeDO/Sources/Models/Priority.swift +++ b/iOS/HalgoraeDO/Sources/Models/Priority.swift @@ -5,7 +5,7 @@ // Created by woong on 2020/11/26. // -import UIKit +import Foundation enum Priority: Int, CaseIterable { case one = 1 @@ -21,33 +21,4 @@ enum Priority: Int, CaseIterable { case .four: return "우선순위 4" } } - - var color: UIColor { - switch self { - case .one: return .red - case .two: return .blue - case .three: return .orange - case .four: return .black - } - } -} - -// MARK: - For PopoverViewModel - -extension Priority { - struct ViewModel: PopoverViewModelType { - var title: String - var tintColor: UIColor? - var image: UIImage? - } - - var viewModel: ViewModel { - let image = UIImage(systemName: "flag.fill")?.scaled(to: .init(width: 30, height: 30)) - switch self { - case .one: return ViewModel(title: title, tintColor: color, image: image) - case .two: return ViewModel(title: title, tintColor: color, image: image) - case .three: return ViewModel(title: title, tintColor: color, image: image) - case .four: return ViewModel(title: title, tintColor: color, image: image) - } - } } From c3846457f931f7b19babb482f624e81df815592f Mon Sep 17 00:00:00 2001 From: Zin0_0 <40550453+pjy0416@users.noreply.github.com> Date: Mon, 30 Nov 2020 10:47:13 +0900 Subject: [PATCH 013/281] =?UTF-8?q?API=20=EC=9D=B4=EC=8A=88=20=ED=83=AC?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20&=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EC=9D=B4=EC=8A=88=20=ED=83=AC=ED=94=8C=EB=A6=BF=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/api.md | 56 ++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/component.md | 18 +++++++++ .github/ISSUE_TEMPLATE/discussion.md | 2 +- 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/api.md create mode 100644 .github/ISSUE_TEMPLATE/component.md diff --git a/.github/ISSUE_TEMPLATE/api.md b/.github/ISSUE_TEMPLATE/api.md new file mode 100644 index 00000000..e0503a41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/api.md @@ -0,0 +1,56 @@ +--- +name: API +about: 백엔드 API 이슈 탬플릿 +title: '작업 - 북마크 생성 API 구현 ' +labels: BE, API +assignees: Zinyon, pjy0416, shkilo + +--- + +## Task - Bookmark POST API +URL +``` +POST /api/task/:taskId/bookmark +``` + +Request +``` +{ + url : 'https://another...' +} +``` + +Request Description +| Name | Required | Type| Description | +| :------------- |:--------------|:------------|:------------| +| grant_type | **REQUIRED** | **CODE** | [RFC 6759#session-6](https://tools.ietf.org/html/rfc6749#section-6) 에 정의 된 인증유형에 대한 구분 값. 현재 다음의 2가지 type 만 사용가능.
- 인증코드 사용시: `authorization_code`
- 리프레시 토큰 사용 시: `refresh_token` | +| client_id | **REQUIRED** | **STRING** | 발급 된 애플리케이션 자격증명의 Client ID 값 | +| client_secret | **REQUIRED** | **STRING** | 발급 된 애플리케이션 자격증명의 Client SECRET 값 | +| code | **CONDITIONAL** | **STRING** | authorization code request 응답으로 부터 발급 받은 인증코드.
grant_type 이 `code` 일 때 만 사용. | +| refresh_token | **CONDITIONAL** | **STRING** | 이전 토큰발급 요청을 통하여 발급 받은 리프레시 토큰.
grant_type 이 `refresh_token ` 일 때 만 사용. | + +Response +``` +{ + 'message': 'ok' +} +``` + +Response Description +``` +| Name | Type | Description | +| :------------- |:--------------|:--------------| +| **primaryEmail** | **STRING** | 사용자 이메일 | +``` + +## API 명세 링크 +https://github.com/boostcamp-2020/Project04-C-Whale/wiki/Task-API + +## error code +| 상태 코드 | 오류 메시지 | 설명 | +|:-----:|:------:|:-----:| +| 400 | Bad Request | 요청이 잘못된 경우 발생합니다. | +| 401 | Unauthorized | 유효한 토큰을 header에 포함하지 않은 경우 발생합니다. | +| 403 | Forbidden | 해당 리소스에 대한 권한이 없는 요청에 대해 발생합니다. | +| 404 | Not Found | 해당 id의 bookmark나 task가 존재하지 않는 경우 발생합니다. | +| 500 | Internal Server Error | 서버에 문제가 생긴 경우 발생합니다. | diff --git a/.github/ISSUE_TEMPLATE/component.md b/.github/ISSUE_TEMPLATE/component.md new file mode 100644 index 00000000..1a24cf1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/component.md @@ -0,0 +1,18 @@ +--- +name: Component +about: Client side 컴포넌트 탬플릿 +title: 로그인 Component 구현 +labels: FE +assignees: Zinyon, pjy0416, shkilo + +--- + +## 화면 기획서 +- [ 화면 기획서 ](https://docs.google.com/presentation/d/1TJlCLFvrsmgS0QGRNYfjvJIuQG5WdjccmZ_hFBwSYX0/edit#slide=id.gac1437662e_0_0 +) +- [ 컴포넌트 설계 기획서 ](https://github.com/boostcamp-2020/Project04-C-Whale/wiki/Component-%EC%84%A4%EA%B3%84-%ED%9A%8C%EC%9D%98) + +## 체크리스트 +- [ ] A +- [ ] B +- [ ] C diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md index c62c4525..6a4700ff 100644 --- a/.github/ISSUE_TEMPLATE/discussion.md +++ b/.github/ISSUE_TEMPLATE/discussion.md @@ -3,7 +3,7 @@ name: Discussion about: 회의를 하면서 고민 사항 및 결정 사항을 이슈로 등록해주세요. title: '' labels: discussion -assignees: SANGYOONLEE, chelwoong, Zinyon, pjy0416, shkilo +assignees: chelwoong, pjy0416, SANGYOONLEE, shkilo, Zinyon --- From 0a3ba95d332b82084788f977d938766f8801e813 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 30 Nov 2020 17:22:43 +0900 Subject: [PATCH 014/281] =?UTF-8?q?[FE]=20FIX:=20eslint-loader=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/vue.config.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/vue.config.js b/client/vue.config.js index 828c5f66..6611b00c 100644 --- a/client/vue.config.js +++ b/client/vue.config.js @@ -1,9 +1,4 @@ module.exports = { transpileDependencies: ["vuetify"], - chainWebpack: (config) => { - config.module.rule('eslint').use('eslint-loader').tap((options) => { - options.fix = true; - return options; - }) - } + lintOnSave: false }; From c9abd02b25f97cc51eaab9180d870cb0c6b4d0d2 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 30 Nov 2020 17:23:47 +0900 Subject: [PATCH 015/281] =?UTF-8?q?[FE]=20REFACTOR:=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EC=B6=95=EC=95=BD=ED=98=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/views/Login.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index 4b412be5..f3c8a09e 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -14,7 +14,7 @@ From 5893d3ca8383d0e56fbf408ad00a8ad714359d1b Mon Sep 17 00:00:00 2001 From: shkilo Date: Mon, 30 Nov 2020 21:49:01 +0900 Subject: [PATCH 021/281] =?UTF-8?q?[FE]=20FEAT:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/project/index.vue | 41 +++++++++++++++++++++++++ client/src/views/Project.vue | 7 +++-- 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 client/src/components/project/index.vue diff --git a/client/src/components/project/index.vue b/client/src/components/project/index.vue new file mode 100644 index 00000000..1b8337da --- /dev/null +++ b/client/src/components/project/index.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/client/src/views/Project.vue b/client/src/views/Project.vue index 836fd6e5..224e85d0 100644 --- a/client/src/views/Project.vue +++ b/client/src/views/Project.vue @@ -1,11 +1,14 @@ From 32f06ee08b5c7423573513a605df8e1c6271c550 Mon Sep 17 00:00:00 2001 From: shkilo Date: Mon, 30 Nov 2020 21:51:05 +0900 Subject: [PATCH 022/281] =?UTF-8?q?[FE]=20FEAT:=20API=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/myAxios.js | 31 +++++++++++++++++++++++++++++++ client/src/api/project.js | 9 +++++++++ 2 files changed, 40 insertions(+) create mode 100644 client/src/api/myAxios.js create mode 100644 client/src/api/project.js diff --git a/client/src/api/myAxios.js b/client/src/api/myAxios.js new file mode 100644 index 00000000..35e077bf --- /dev/null +++ b/client/src/api/myAxios.js @@ -0,0 +1,31 @@ +import axios from "axios"; + +const headerConfig = { + header: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, +}; + +const BASE_URL = process.env.VUE_APP_BASE_URL; + +const GET = (path) => { + return axios.get(BASE_URL + path, headerConfig); +}; + +const POST = (path, body) => { + return axios.post(BASE_URL + path, body, headerConfig); +}; + +const PATCH = (path, body) => { + return axios.patch(BASE_URL + path, body, headerConfig); +}; + +const PUT = (path, body) => { + return axios.put(BASE_URL + path, body, headerConfig); +}; + +const DELETE = (path) => { + return axios.delete(BASE_URL + path, headerConfig); +}; + +export default { GET, POST, PATCH, PUT, DELETE }; diff --git a/client/src/api/project.js b/client/src/api/project.js new file mode 100644 index 00000000..b5b93276 --- /dev/null +++ b/client/src/api/project.js @@ -0,0 +1,9 @@ +import myAxios from "./myAxios"; + +const projectAPI = { + getProjectById(projectId) { + return myAxios.GET(`/project/${projectId}`); + }, +}; + +export default projectAPI; From 4887d8a1875e13295d71c996fedaa3e0df79971f Mon Sep 17 00:00:00 2001 From: shkilo Date: Mon, 30 Nov 2020 21:53:10 +0900 Subject: [PATCH 023/281] =?UTF-8?q?[FE]=20FEAT:=20Menu=20component=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/menu/index.vue | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 client/src/components/menu/index.vue diff --git a/client/src/components/menu/index.vue b/client/src/components/menu/index.vue new file mode 100644 index 00000000..1890b195 --- /dev/null +++ b/client/src/components/menu/index.vue @@ -0,0 +1,19 @@ + + + + + From 8f01042a214c96c8486a52119ec107e5fe08a457 Mon Sep 17 00:00:00 2001 From: shkilo Date: Mon, 30 Nov 2020 21:53:36 +0900 Subject: [PATCH 024/281] =?UTF-8?q?[FE]=20TEST:=20project=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9E=84=EC=8B=9C=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/App.vue b/client/src/App.vue index b5321e4f..6bc44c18 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -14,10 +14,12 @@ export default { const accessToken = location.search.split("token=")[1]; if (accessToken) { localStorage.setItem("token", accessToken); + // this.$store.commit('LOGIN', accessToken); location.href = "/"; } if (localStorage.getItem("token")) { - router.push("/today").catch(() => {}); + // router.push("/today").catch(() => {}); + router.push("/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f").catch(() => {}); } else { router.push("/login").catch(() => {}); } From d87f02ed8ce6bae066e17924e54c12b461de5449 Mon Sep 17 00:00:00 2001 From: shkilo Date: Mon, 30 Nov 2020 21:57:42 +0900 Subject: [PATCH 025/281] =?UTF-8?q?[FE]=20FEAT:=20axios=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/package.json b/client/package.json index 11c35958..ee470f9c 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "axios": "^0.21.0", "core-js": "^3.6.5", "vue": "^2.6.11", "vue-router": "^3.2.0", From d5a6a4a92dab0a709abdda21599c79b92addc010 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 30 Nov 2020 22:26:41 +0900 Subject: [PATCH 026/281] =?UTF-8?q?[BE]=20CHORE:=20concurrent=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98=20=EB=B0=8F=20script=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/package-lock.json | 229 ++++++++++++++++++++++++++++++++++++++- server/package.json | 4 +- 2 files changed, 231 insertions(+), 2 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 14100614..0c07f040 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,6 +1,6 @@ { "name": "halgoraedo", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1998,6 +1998,191 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concurrently": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "date-fns": "^2.0.1", + "lodash": "^4.17.15", + "read-pkg": "^4.0.1", + "rxjs": "^6.5.2", + "spawn-command": "^0.0.2-1", + "supports-color": "^6.1.0", + "tree-kill": "^1.2.2", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dev": true, + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "config-chain": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", @@ -2078,6 +2263,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2152,6 +2346,12 @@ "whatwg-url": "^8.0.0" } }, + "date-fns": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", + "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -5265,6 +5465,12 @@ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -7052,6 +7258,15 @@ "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", "dev": true }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -7685,6 +7900,12 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, "spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -8252,6 +8473,12 @@ "punycode": "^2.1.1" } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "tsconfig-paths": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", diff --git a/server/package.json b/server/package.json index 84519336..ccfb7f38 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,8 @@ "dev": "nodemon ./src/app.js", "seed": "npx sequelize-cli db:seed:all", "unseed": "npx sequelize-cli db:seed:undo:all", - "test": "jest --detectOpenHandles --forceExit" + "test": "jest --detectOpenHandles --forceExit", + "concurrent": "concurrently \"npm run dev\" \"npm run serve --prefix ../client\"" }, "jest": { "moduleNameMapper": { @@ -42,6 +43,7 @@ "uuid": "^8.3.1" }, "devDependencies": { + "concurrently": "^5.3.0", "eslint": "^7.13.0", "eslint-config-airbnb-base": "^14.2.0", "eslint-config-prettier": "^6.15.0", From 48c49de7ba287c7ca04a3316ea6a911552c16c33 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 30 Nov 2020 22:27:18 +0900 Subject: [PATCH 027/281] =?UTF-8?q?[BE]=20FIX:=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20CLIENT=5FDOMAIN=5FDEVELOP=20=3D>=20CLIENT?= =?UTF-8?q?=5FURL=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/user.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/server/src/controllers/user.js b/server/src/controllers/user.js index 12dbafd7..bff1b54d 100644 --- a/server/src/controllers/user.js +++ b/server/src/controllers/user.js @@ -2,15 +2,12 @@ const { responseHandler } = require('@utils/handler'); const { createJWT } = require('@utils/auth'); const naverLogin = (req, res, next) => { - const clientURL = - process.env.NODE_ENV === 'development' - ? process.env.CLIENT_DOMAIN_DEVELOP - : process.env.CLIENT_DOMAIN_PRODUCTION; + const { CLIENT_URL } = process.env; try { const { user } = req; const token = createJWT(user); res.header('Authentication', token); - res.status(200).redirect(`${clientURL}?token=${token}`); + res.status(200).redirect(`${CLIENT_URL}?token=${token}`); } catch (err) { next(err); } From 0c412c596b41f4ee3a27c58549cee70a5a3b5eab Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 30 Nov 2020 23:17:48 +0900 Subject: [PATCH 028/281] =?UTF-8?q?[FE]=20REFACTOR:=20myAxios=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20=EA=B0=9D=EC=B2=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다른 모듈들과 일관성을 유지하기 위해 객체화 --- client/package-lock.json | 13 ++++++++++--- client/src/api/myAxios.js | 38 ++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 6de6780b..8721879a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.1.0", + "version": "0.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2981,6 +2981,14 @@ "integrity": "sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk=", "dev": true }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz", @@ -6747,8 +6755,7 @@ "follow-redirects": { "version": "1.13.0", "resolved": "https://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.13.0.tgz", - "integrity": "sha1-tC6Nk6Kn7qXtiGM2dtZZe8jjhNs=", - "dev": true + "integrity": "sha1-tC6Nk6Kn7qXtiGM2dtZZe8jjhNs=" }, "for-in": { "version": "1.0.2", diff --git a/client/src/api/myAxios.js b/client/src/api/myAxios.js index 35e077bf..598cc4de 100644 --- a/client/src/api/myAxios.js +++ b/client/src/api/myAxios.js @@ -6,26 +6,24 @@ const headerConfig = { }, }; -const BASE_URL = process.env.VUE_APP_BASE_URL; +const BASE_URL = process.env.VUE_APP_SERVER_URL; -const GET = (path) => { - return axios.get(BASE_URL + path, headerConfig); -}; - -const POST = (path, body) => { - return axios.post(BASE_URL + path, body, headerConfig); -}; - -const PATCH = (path, body) => { - return axios.patch(BASE_URL + path, body, headerConfig); -}; - -const PUT = (path, body) => { - return axios.put(BASE_URL + path, body, headerConfig); -}; - -const DELETE = (path) => { - return axios.delete(BASE_URL + path, headerConfig); +const myAxios = { + GET: (path) => { + return axios.get(BASE_URL + path, headerConfig); + }, + POST: (path, body) => { + return axios.post(BASE_URL + path, body, headerConfig); + }, + PATCH: (path, body) => { + return axios.patch(BASE_URL + path, body, headerConfig); + }, + PUT: (path, body) => { + return axios.put(BASE_URL + path, body, headerConfig); + }, + DELETE: (path) => { + return axios.delete(BASE_URL + path, headerConfig); + }, }; -export default { GET, POST, PATCH, PUT, DELETE }; +export default myAxios; From f448c30f66fe8711f7447a2ce104b71c5c7a52b6 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 30 Nov 2020 23:18:21 +0900 Subject: [PATCH 029/281] =?UTF-8?q?[FE]=20FIX:=20prettier=20CRLF=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - eslint rules에 endOfLine : auto 옵션 추가 --- client/.eslintrc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 829a939d..ba734f57 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -9,7 +9,8 @@ module.exports = { }, rules: { "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", - "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" + "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", + "prettier/prettier": ['error', {endOfLine: 'auto'}] }, overrides: [ { From 8538e28c8b90b57bbed318e5b40f36d8d6280f59 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 1 Dec 2020 10:15:19 +0900 Subject: [PATCH 030/281] =?UTF-8?q?[FE]=20FEAT:=20project=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=98=ED=94=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - toggle 되는 부분 수정 필요 --- client/src/components/project/index.vue | 41 +++++++++++++++++-------- client/src/views/Project.vue | 4 +-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/client/src/components/project/index.vue b/client/src/components/project/index.vue index 1b8337da..24840b56 100644 --- a/client/src/components/project/index.vue +++ b/client/src/components/project/index.vue @@ -1,25 +1,36 @@ From 6728d128baff24656a2c8f333627dd26c1a1f18c Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 1 Dec 2020 17:50:46 +0900 Subject: [PATCH 051/281] =?UTF-8?q?[FE]=20FEAT:=20auth=20store=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SET_TOKEN, SET_USER, LOGOUT의 mutation 구현 - checkuser 비동기 로직 구현 --- client/src/store/auth.js | 50 +++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/client/src/store/auth.js b/client/src/store/auth.js index 0b4f3ed8..4cfaf0e9 100644 --- a/client/src/store/auth.js +++ b/client/src/store/auth.js @@ -1,18 +1,56 @@ +import router from "@/router/index.js"; +import userAPI from "@/api/user.js"; + const state = { - accessToken: null, + user: { + id: "", + name: "", + email: "", + }, }; + const mutations = { - LOGIN(state, { accessToken }) { - state.accessToken = accessToken; + SET_TOKEN(state, accessToken) { localStorage.setItem("token", accessToken); + location.replace("/"); }, - LOGOUT(state) { - state.accessToken = null; + SET_USER(state, user) { + state.user = { + id: user.id, + name: user.name, + email: user.email, + }; + }, + LOGOUT() { localStorage.removeItem("token"); + location.replace("/"); }, }; -const actions = {}; +const actions = { + setToken({ commit }, { accessToken }) { + commit("SET_TOKEN", accessToken); + }, + + async checkUser({ commit }) { + try { + const { data: user } = await userAPI.authorize(); + commit("SET_USER", user); + + if (location.pathname === "/") { + router.replace("/today"); + } else { + router.replace(location.pathname); + } + } catch (err) { + alert("로그인에 실패했습니다"); + } + }, + + logout({ commit }) { + commit("LOGOUT"); + }, +}; export default { state, From 61ee745c49f14a18e2c900109c654b7c758ee97f Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 1 Dec 2020 17:51:22 +0900 Subject: [PATCH 052/281] =?UTF-8?q?[BE]=20REFACTOR:=20task=20api=20test=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20task=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/task.api.test.js | 304 +++++++++++++++++++---------------- 1 file changed, 164 insertions(+), 140 deletions(-) diff --git a/server/test/task.api.test.js b/server/test/task.api.test.js index d3bf3ecb..62219019 100644 --- a/server/test/task.api.test.js +++ b/server/test/task.api.test.js @@ -2,7 +2,8 @@ require('module-alias/register'); const request = require('supertest'); const app = require('@root/app'); const seeder = require('@test/test-seed'); -const { projects, tasks, labels, priorities, alarms } = require('@test/test-seed'); +const status = require('@test/response-status'); +const { createJWT } = require('@utils/auth'); beforeAll(async done => { await seeder.up(); @@ -14,25 +15,44 @@ afterAll(async done => { done(); }); -const SUCCESS_CODE = 201; -const SUCCESS_MSG = 'ok'; +describe('get All task', () => { + it('성공 조건', async done => { + // given + const expectedTasks = []; + try { + // when + const res = await request(app) + .get('/api/task') + .set('Authorization', createJWT(seeder.users[0])); + + const { tasks } = res.body; + // then + expect(tasks).toStrictEqual(expectedTasks); + } catch (err) { + done(err); + } + }); +}); describe('get task by id', () => { - it('get task by id 일반', done => { - const expectedChildTaskId = '8d62f93c-9233-46a9-a5cf-ec18ad5a36f4'; + it('get task by id 일반', async done => { + // given + const taskId = seeder.tasks[0].id; + const expectedChildren = seeder.tasks.filter(task => task.parentId === taskId); try { - request(app) - .get('/api/task/13502adf-83dd-4e8e-9acf-5c5a0abd5b1b') - .end((err, res) => { - if (err) { - throw err; - } - - const firstChildTaskId = res.body.tasks[0].id; - expect(firstChildTaskId).toEqual(expectedChildTaskId); - done(); - }); + // when + const res = await request(app).get(`/api/task/${taskId}`); + const recievedChildren = res.body.tasks.filter(task => task.parentId === taskId); + + // then + recievedChildren.forEach(recievedChild => { + expect( + expectedChildren.some(expectedChild => recievedChild.id === expectedChild.id), + ).toBeTruthy(); + }); + + done(); } catch (err) { done(err); } @@ -44,18 +64,21 @@ describe('post task', () => { // given const newTask = { title: '할일', - projectId: projects[0].id, - labelIdList: JSON.stringify(labels.map(label => label.id)), - priorityId: priorities[0].id, - dueDate: '2021-11-28', + projectId: seeder.projects[0].id, + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), parentId: null, - alarmId: alarms[0].id, + alarmId: seeder.alarms[0].id, position: 1, }; + // when const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); done(); }); @@ -64,18 +87,20 @@ describe('post task', () => { const newTask = { title: '할일', projectId: null, - labelIdList: JSON.stringify(labels.map(label => label.id)), - priorityId: priorities[0].id, - dueDate: '2020-11-28', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), parentId: null, - alarmId: alarms[0].id, + alarmId: seeder.alarms[0].id, position: 1, }; + // when const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); done(); }); @@ -83,19 +108,21 @@ describe('post task', () => { // given const newTask = { title: '할일', - projectId: projects[0].id, + projectId: seeder.projects[0].id, labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, - dueDate: '2020-11-28', + priorityId: seeder.priorities[1].id, + dueDate: new Date(), parentId: null, - alarmId: alarms[0].id, + alarmId: seeder.alarms[0].id, position: 1, }; + // when const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); done(); }); @@ -103,19 +130,21 @@ describe('post task', () => { // given const newTask = { title: '할일', - projectId: projects[0].id, + projectId: seeder.projects[0].id, labelIdList: JSON.stringify([]), priorityId: null, - dueDate: '2020-11-28', + dueDate: new Date(), parentId: null, - alarmId: alarms[0].id, + alarmId: seeder.alarms[0].id, position: 1, }; + // when const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); done(); }); @@ -123,19 +152,21 @@ describe('post task', () => { // given const newTask = { title: '할일', - projectId: projects[1].id, + projectId: seeder.projects[1].id, labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, - dueDate: '2020-11-28', - parentId: tasks[0].id, - alarmId: alarms[0].id, + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: seeder.tasks[0].id, + alarmId: seeder.alarms[0].id, position: 1, }; + // when const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); done(); }); @@ -143,19 +174,21 @@ describe('post task', () => { // given const newTask = { title: '할일', - projectId: projects[1].id, + projectId: seeder.projects[1].id, labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, - dueDate: '2020-11-28', - parentId: tasks[0].id, + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: seeder.tasks[0].id, alarmId: null, position: 1, }; + // when const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); done(); }); @@ -163,48 +196,47 @@ describe('post task', () => { // given const newTask = { title: '할일', - projectId: projects[1].id, + projectId: seeder.projects[1].id, labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, + priorityId: seeder.priorities[1].id, dueDate: '2020-10-28', - parentId: tasks[0].id, + parentId: seeder.tasks[0].id, alarmId: null, position: 1, }; + // when const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(400); + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); expect(res.body.message).toBe('유효하지 않은 dueDate'); done(); }); }); -describe('post task with id (업데이트)', () => { - it('post task with id 일반', done => { +describe('patch task with id', () => { + it('patch task with id 일반', async done => { + // given const newTask = { title: '할일', - projectId: projects[0].id, - labelIdList: JSON.stringify(labels.map(label => label.id)), - priorityId: priorities[0].id, - dueDate: '2021-11-28', + projectId: seeder.projects[0].id, + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), parentId: null, - alarmId: alarms[0].id, + alarmId: seeder.alarms[0].id, position: 1, }; try { - request(app) - .post('/api/task/13502adf-83dd-4e8e-9acf-5c5a0abd5b1b') - .send(newTask) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); - }); + // when + const res = await request(app).patch(`/api/task/${seeder.tasks[1].id}`).send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } @@ -212,18 +244,17 @@ describe('post task with id (업데이트)', () => { }); describe('delete task', () => { - it('delete task 일반', done => { + it('delete task 일반', async done => { + // given + const taskId = seeder.tasks[0].id; try { - request(app) - .delete('/api/task/13502adf-83dd-4e8e-9acf-5c5a0abd5b1b') - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); - }); + // when + const res = await request(app).delete(`/api/task/${taskId}`); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } @@ -231,21 +262,19 @@ describe('delete task', () => { }); describe('get comments', () => { - it('get comments 일반', done => { - const expectedCommentId = '6200bcb9-f871-439b-9507-57abbde3d468'; + it('get comments 일반', async done => { + // given + const expectedCommentId = seeder.comments[0].id; + const taskId = seeder.tasks[1].id; try { - request(app) - .get('/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment') - .end((err, res) => { - if (err) { - throw err; - } - - const firstCommentId = res.body[0].id; - expect(firstCommentId).toEqual(expectedCommentId); - done(); - }); + // when + const res = await request(app).get(`/api/task/${taskId}/comment`); + const firstCommentId = res.body[0].id; + + // then + expect(firstCommentId).toEqual(expectedCommentId); + done(); } catch (err) { done(err); } @@ -253,23 +282,21 @@ describe('get comments', () => { }); describe('create comment', () => { - it('create comment 일반', done => { + it('create comment 일반', async done => { + // given const requestBody = { content: '새로운 댓글', }; + const taskId = seeder.tasks[1].id; try { - request(app) - .post('/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); + // when + const res = await request(app).post(`/api/task/${taskId}/comment`).send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } @@ -277,25 +304,23 @@ describe('create comment', () => { }); describe('update comment', () => { - it('update comment 일반', done => { + it('update comment 일반', async done => { + // given const requestBody = { content: '바뀐 댓글', }; - + const taskId = seeder.tasks[1].id; + const commentId = seeder.comments[0].id; try { - request(app) - .put( - '/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment/6200bcb9-f871-439b-9507-57abbde3d468', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); + // when + const res = await request(app) + .put(`/api/task/${taskId}/comment/${commentId}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } @@ -303,25 +328,24 @@ describe('update comment', () => { }); describe('delete comment', () => { - it('delete comment 일반', done => { + it('delete comment 일반', async done => { + // given const requestBody = { content: '바뀐 댓글', }; + const taskId = seeder.tasks[1].id; + const commentId = seeder.comments[0].id; try { - request(app) - .delete( - '/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment/6200bcb9-f871-439b-9507-57abbde3d468', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); + // when + const res = await request(app) + .delete(`/api/task/${taskId}/comment/${commentId}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } From 8597610f396ba7f63b8dff11e7ffbb9587306f7a Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 1 Dec 2020 17:51:36 +0900 Subject: [PATCH 053/281] =?UTF-8?q?[FE]=20FEAT:=20private=20router=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - route beforeEnter 라이프사이클에 토큰 유무로 권한 체크 로직 추가 --- client/src/router/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/router/index.js b/client/src/router/index.js index c50bfd2f..27da040d 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -8,6 +8,11 @@ import Home from "../views/Home.vue"; Vue.use(VueRouter); +const requireAuth = () => (from, to, next) => { + if (localStorage.getItem("token")) return next(); + next("/login"); +}; + const routes = [ { path: "/login", @@ -23,16 +28,19 @@ const routes = [ path: "today", name: "Today", component: Today, + beforeEnter: requireAuth(), }, { path: "project/:projectId", name: "Project", component: Project, + beforeEnter: requireAuth(), }, { path: "task/:taskId", name: "Task", component: Task, + beforeEnter: requireAuth(), }, ], }, From 5cb3031593fc40f92bcf4ba93a8d2a0545babc19 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 1 Dec 2020 17:52:44 +0900 Subject: [PATCH 054/281] =?UTF-8?q?[FE]=20FEAT:=20userAPI=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/user.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/api/user.js b/client/src/api/user.js index e69de29b..33755579 100644 --- a/client/src/api/user.js +++ b/client/src/api/user.js @@ -0,0 +1,9 @@ +import myAxios from "./myAxios"; + +const userAPI = { + authorize() { + return myAxios.GET("/user/me"); + }, +}; + +export default userAPI; From 9350b86e342616f4dce59107411337cef1303530 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 1 Dec 2020 17:52:59 +0900 Subject: [PATCH 055/281] =?UTF-8?q?[FE]=20FEAT:=20App=20created=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - url로 토큰 넘어올시 저장하고 redirect - localStorgae 토큰 없으면 login 페이지로 라우팅 - 토큰 있으면 인증 후 해당 pathname으로 라우팅 --- client/src/App.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/App.vue b/client/src/App.vue index 6bc44c18..8ebbdfc4 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -4,6 +4,7 @@ diff --git a/client/src/store/auth.js b/client/src/store/auth.js index a772641f..01123e57 100644 --- a/client/src/store/auth.js +++ b/client/src/store/auth.js @@ -10,10 +10,6 @@ const state = { }; const mutations = { - SET_TOKEN(state, accessToken) { - localStorage.setItem("token", accessToken); - location.replace("/"); - }, SET_USER(state, user) { state.user = { id: user.id, @@ -28,19 +24,15 @@ const mutations = { }; const actions = { - setToken({ commit }, { accessToken }) { - commit("SET_TOKEN", accessToken); - }, - async checkUser({ commit }) { try { const { data: user } = await userAPI.authorize(); commit("SET_USER", user); if (location.pathname === "/") { - router.replace("/today"); + router.replace("/today").catch(() => {}); } else { - router.replace(location.pathname); + router.replace(location.pathname).catch(() => {}); } } catch (err) { alert("로그인에 실패했습니다"); From 6ea332d92e73b9c5ef37206541c18ccd8110ad7f Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 1 Dec 2020 23:06:01 +0900 Subject: [PATCH 075/281] =?UTF-8?q?[FE]=20FEAT:=20redirectHome=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - login 페이지 접근시 token이 있으면 home으로 가도록 --- client/src/router/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/router/index.js b/client/src/router/index.js index 27da040d..25aa5b67 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -13,11 +13,17 @@ const requireAuth = () => (from, to, next) => { next("/login"); }; +const redirectHome = () => (from, to, next) => { + if (localStorage.getItem("token")) return next("/"); + next(); +}; + const routes = [ { path: "/login", name: "Login", component: Login, + beforeEnter: redirectHome(), }, { path: "/", From 3b91ba22830d47fa5f7fba9b08b080b3b9aafe1d Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 2 Dec 2020 01:25:15 +0900 Subject: [PATCH 076/281] =?UTF-8?q?[BE]=20CHORE:=20task=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=9D=84=20=EC=9C=84=ED=95=9C=20seed=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/seeders/index.js | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/server/src/models/seeders/index.js b/server/src/models/seeders/index.js index 193bc623..f69b1ff9 100644 --- a/server/src/models/seeders/index.js +++ b/server/src/models/seeders/index.js @@ -15,6 +15,14 @@ const users = [ createdAt: new Date(), updatedAt: new Date(), }, + { + id: '58719e77-5f55-4ca7-9552-ff94fbb07ea4', + email: 'dimple0416@naver.com', + name: '박진영', + provider: 'naver', + createdAt: new Date(), + updatedAt: new Date(), + }, ]; const priorities = [ @@ -63,6 +71,15 @@ const projects = [ createdAt: new Date(), updatedAt: new Date(), }, + { + id: '082b92fb-dce7-4249-9467-2f261767196b', + creatorId: users[2].id, + title: '내 프로젝트', + isList: false, + isFavorite: true, + createdAt: new Date(), + updatedAt: new Date(), + }, ]; const sections = [ @@ -82,6 +99,14 @@ const sections = [ createdAt: new Date(), updatedAt: new Date(), }, + { + id: 'dbfc6305-877e-46b9-ac63-d56fb82681dd', + title: '섹션 3', + projectId: projects[2].id, + position: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, ]; const tasks = [ @@ -166,6 +191,17 @@ const tasks = [ createdAt: new Date(), updatedAt: new Date(), }, + { + id: '4a83b457-e67e-43d8-a284-78ea7fc440d5', + projectId: projects[2].id, + sectionId: sections[2].id, + title: '진영', + dueDate: new Date(), + position: 1, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, ]; const comments = [ From f74864bf692d8b19368e2ae224f420a267cf9b40 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 2 Dec 2020 01:26:01 +0900 Subject: [PATCH 077/281] =?UTF-8?q?[FE]=20FIX:=20header=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/myAxios.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/api/myAxios.js b/client/src/api/myAxios.js index 94679b96..9b38a775 100644 --- a/client/src/api/myAxios.js +++ b/client/src/api/myAxios.js @@ -1,7 +1,7 @@ import axios from "axios"; const headerConfig = { - header: { + headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, }; From 982442c6e304dea812ef5b8b74fef24c449b3cff Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 2 Dec 2020 01:27:02 +0900 Subject: [PATCH 078/281] =?UTF-8?q?[FE]=20REFACTOR:=20API=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EB=B0=8F=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/task.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/api/task.js b/client/src/api/task.js index 892526ef..8fb94d36 100644 --- a/client/src/api/task.js +++ b/client/src/api/task.js @@ -1,12 +1,12 @@ import myAxios from "./myAxios"; const taskAPI = { + serachTask() { + return myAxios.GET("/task"); + }, updateTask(taskId, data) { return myAxios.PATCH(`/task/${taskId}`, data); }, - serachTask(keyword, data) { - return myAxios.PATCH(`/task/serach?keyword=${keyword}`, data); - }, }; export default taskAPI; From 7859001825a8f3333c7471bdf42e02af983aebdc Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 2 Dec 2020 01:27:44 +0900 Subject: [PATCH 079/281] =?UTF-8?q?[FE]=20REFACTOR:=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=20API=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/Search.vue | 51 +++++++++------------------ 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/client/src/components/task/Search.vue b/client/src/components/task/Search.vue index 98bc9c7c..4c0077b5 100644 --- a/client/src/components/task/Search.vue +++ b/client/src/components/task/Search.vue @@ -10,7 +10,7 @@ color="green" hide-no-data hide-selected - item-text="Description" + item-text="title" item-value="API" placeholder="검색" prepend-icon="mdi-magnify" @@ -18,12 +18,6 @@ solo > - {{ task.title }} @@ -33,17 +27,14 @@ From cb81152540fb1976fb1618113c668a0b3cf1cbf1 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 02:15:17 +0900 Subject: [PATCH 082/281] =?UTF-8?q?[FE]=20FEAT:=20Home.vue=20-=20navigatio?= =?UTF-8?q?n-drawer=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - header보다 내려오게 - color값 수정 --- client/src/views/Home.vue | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 53a96751..1e4db230 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,13 +1,13 @@ + + + {{ project.title }} {{ project.taskCount }} + + From 7eec04fb7f3aa88e8c927174f857bab08a8d0238 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 04:18:27 +0900 Subject: [PATCH 086/281] =?UTF-8?q?[FE]=20REFACTOR:=20auth=20store=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - login 라우팅인데 인증이 되었을 경우 today로 가게 --- client/src/store/auth.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/store/auth.js b/client/src/store/auth.js index 01123e57..18a5c4b9 100644 --- a/client/src/store/auth.js +++ b/client/src/store/auth.js @@ -29,14 +29,17 @@ const actions = { const { data: user } = await userAPI.authorize(); commit("SET_USER", user); - if (location.pathname === "/") { + if (location.pathname === "/" || location.pathname === "/login") { router.replace("/today").catch(() => {}); + return; } else { router.replace(location.pathname).catch(() => {}); + return; } } catch (err) { - alert("로그인에 실패했습니다"); - router.replace("/login"); + alert("로그인에 실패했습니다."); + router.replace("/login").catch(() => {}); + return; } }, From 280bcc0ba02ad7f2398f166ccdb4b92f22d51029 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 04:19:14 +0900 Subject: [PATCH 087/281] =?UTF-8?q?[FE]=20FEAT:=20project=20store=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getProjects 관련 action, mutation, state 구현 --- client/src/api/project.js | 3 +++ client/src/store/menu.js | 29 ----------------------------- client/src/store/project.js | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 29 deletions(-) delete mode 100644 client/src/store/menu.js diff --git a/client/src/api/project.js b/client/src/api/project.js index b5b93276..ad7154e1 100644 --- a/client/src/api/project.js +++ b/client/src/api/project.js @@ -4,6 +4,9 @@ const projectAPI = { getProjectById(projectId) { return myAxios.GET(`/project/${projectId}`); }, + getProjects() { + return myAxios.GET("/project"); + }, }; export default projectAPI; diff --git a/client/src/store/menu.js b/client/src/store/menu.js deleted file mode 100644 index 8fb92392..00000000 --- a/client/src/store/menu.js +++ /dev/null @@ -1,29 +0,0 @@ -import axios from "axios"; - -const state = { - projectInfos: [], -}; - -const getters = { - projectInfos: (state) => state.projectInfos, -}; - -const actions = { - async fetchProjectInfos({ commit }) { - const response = await axios.get("http://localhost:3000/api/project"); - console.log(response); - // commit("setTodos", response.data); - commit(); - }, -}; - -const mutations = { - setProjectInfos: (state, projectInfos) => (state.projectInfos = projectInfos), -}; - -export default { - state, - getters, - actions, - mutations, -}; diff --git a/client/src/store/project.js b/client/src/store/project.js index 45ac2573..898752f2 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -8,10 +8,13 @@ const state = { isList: null, sections: [], }, + projectInfos: [], }; const getters = { currentProject: (state) => state.currentProject, + projectInfos: (state) => state.projectInfos, + managedProject: (state) => state.projects.filter((project) => project.title === "관리함"), }; const actions = { @@ -37,10 +40,21 @@ const actions = { alert("프로젝트 조회 요청 실패"); } }, + async fetchProjectInfos({ commit }) { + try { + const { data: projects } = await projectAPI.getProjects(); + + commit("setProjects", projects); + } catch (err) { + alert("프로젝트 전체 정보 조회 요청 실패"); + } + }, }; const mutations = { + //TODO: function vs arrow-function style-guide 보고 통일하기 setCurrentProject: (state, currentProject) => (state.currentProject = currentProject), + setProjects: (state, projects) => (state.projects = projects), // newTodo: (state, todo) => state.todos.unshift(todo), }; From f65f39059091ed36b6d9f53ecfa81dbf67fc6791 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 04:19:53 +0900 Subject: [PATCH 088/281] =?UTF-8?q?[FE]=20FEAT:=20task=20store=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getAllTasks 관련 state, mutations, actions 구현 - 진행중 --- client/src/api/task.js | 7 +++++-- client/src/store/index.js | 3 +-- client/src/store/task.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 client/src/store/task.js diff --git a/client/src/api/task.js b/client/src/api/task.js index c02b161a..075c1bf0 100644 --- a/client/src/api/task.js +++ b/client/src/api/task.js @@ -1,9 +1,12 @@ import myAxios from "./myAxios"; -const projectAPI = { +const taskAPI = { updateTask(taskId, data) { return myAxios.PATCH(`/task/${taskId}`, data); }, + getTasks() { + return myAxios.get("/task/all"); + }, }; -export default projectAPI; +export default taskAPI; diff --git a/client/src/store/index.js b/client/src/store/index.js index 746d2514..d616f102 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -2,10 +2,9 @@ import Vue from "vue"; import Vuex from "vuex"; import auth from "./auth"; import project from "./project"; -import menu from "./menu"; Vue.use(Vuex); export default new Vuex.Store({ - modules: { auth, menu, project }, + modules: { auth, project }, }); diff --git a/client/src/store/task.js b/client/src/store/task.js new file mode 100644 index 00000000..eef749e3 --- /dev/null +++ b/client/src/store/task.js @@ -0,0 +1,29 @@ +import projectAPI from "../api/project"; +import taskAPI from "../api/task"; + +const state = { + newTask: {}, + tasks: [], +}; + +const getters = {}; + +const actions = { + async fetchAllTasks({ commit }) { + try { + const tasks = await taskAPI.getTasks(); + commit("setTasks", tasks); + } catch (err) { + alert("작업 전체 조회 요청 실패"); + } + }, +}; + +const mutations = {}; + +export default { + state, + getters, + actions, + mutations, +}; From 99c7d3115ba13797e31272cc0d292b35cd0ea0a9 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 04:20:40 +0900 Subject: [PATCH 089/281] =?UTF-8?q?[BE]=20FEAT:=20task=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - task 전체 조회 후 오늘, 다음으로 클라이언트에서 분류하면 좋을 것 같습니다 --- server/src/controllers/task.js | 19 +++++++++++++++++++ server/src/routes/project.js | 3 +-- server/src/routes/task.js | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index ec8d9e08..d8644c1d 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -4,6 +4,24 @@ const { asyncTryCatch } = require('@utils/async-try-catch'); const { responseHandler } = require('@utils/handler'); const { isValidDueDate } = require('@utils/date'); +const getTasks = asyncTryCatch(async (req, res) => { + const tasks = await models.task.findAll({ + include: [ + 'labels', + 'priority', + 'alarm', + 'bookmarks', + { + model: models.task, + include: ['labels', 'priority', 'alarm', 'bookmarks'], + }, + ], + order: [[models.task, 'position', 'ASC']], + }); + + responseHandler(res, 200, tasks); +}); + const getTaskById = asyncTryCatch(async (req, res) => { const task = await models.task.findByPk(req.params.taskId, { include: [ @@ -120,6 +138,7 @@ const deleteComment = asyncTryCatch(async (req, res) => { }); module.exports = { + getTasks, getTaskById, createTask, updateTask, diff --git a/server/src/routes/project.js b/server/src/routes/project.js index e975cdb7..99d5bc65 100644 --- a/server/src/routes/project.js +++ b/server/src/routes/project.js @@ -2,8 +2,7 @@ const router = require('express').Router(); const projectController = require('@controllers/project'); router.get('/', projectController.getProjects); -// to do -// router.get('/today', projectController.getTodayProject); +// TODO: router.get('/today', projectController.getTodayProject); router.get('/:projectId', projectController.getProjectById); router.post('/', projectController.createProject); router.put('/:projectId', projectController.updateProject); diff --git a/server/src/routes/task.js b/server/src/routes/task.js index 32beca9e..24b3069e 100644 --- a/server/src/routes/task.js +++ b/server/src/routes/task.js @@ -1,6 +1,7 @@ const router = require('express').Router(); const taskController = require('@controllers/task'); +router.get('/all', taskController.getTasks); router.get('/:taskId', taskController.getTaskById); router.post('/', taskController.createTask); router.patch('/:taskId', taskController.updateTask); From 8e5956bb9d4ad2492751ba84fce72a27ff08e82a Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 04:21:54 +0900 Subject: [PATCH 090/281] =?UTF-8?q?[FE]=20todayTask=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/task.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/store/task.js b/client/src/store/task.js index eef749e3..a5a92d78 100644 --- a/client/src/store/task.js +++ b/client/src/store/task.js @@ -6,7 +6,9 @@ const state = { tasks: [], }; -const getters = {}; +const getters = { + todayTasks: (state) => state.tasks.filter(new Date(task.dueDate) < new Date(Date.now()) ) +}; const actions = { async fetchAllTasks({ commit }) { @@ -19,7 +21,9 @@ const actions = { }, }; -const mutations = {}; +const mutations = { + setTasks: (state, tasks) => state.tasks= tasks; +}; export default { state, From 710026ea88352d8f6bc46bf2ce9d25d2e39e2b6b Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 10:25:48 +0900 Subject: [PATCH 091/281] =?UTF-8?q?[BE]=20FIX:=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EC=9E=91=EC=97=85=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20API=20=EB=9D=BC=EC=9A=B0=ED=84=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/routes/project.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/routes/project.js b/server/src/routes/project.js index ebf217ee..4987505f 100644 --- a/server/src/routes/project.js +++ b/server/src/routes/project.js @@ -12,7 +12,10 @@ router.patch('/:projectId', projectController.updateProject); router.delete('/:projectId', projectController.deleteProject); router.post('/:projectId/section', projectController.createSection); -router.post('/:projectId/section/:sectionId/task', projectController.updateSectionTaskPositions); +router.patch( + '/:projectId/section/:sectionId/position', + projectController.updateSectionTaskPositions, +); router.put('/:projectId/section/:sectionId', projectController.updateSection); router.delete('/:projectId/section/:sectionId', projectController.deleteSection); From ef6126246e82ccd70482d39b698fe0a18aa5ca31 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 10:26:22 +0900 Subject: [PATCH 092/281] =?UTF-8?q?[FE]=20FIX:=20oAuth=20=3D>=20OAuth?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/views/Login.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index 902ec669..a93e7339 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -3,7 +3,7 @@
@@ -16,7 +16,7 @@ export default { name: "Login", data() { return { - oAuthURL: process.env.VUE_APP_SERVER_URL + "/user/oauth/naver", + OAuthURL: process.env.VUE_APP_SERVER_URL + "/user/oauth/naver", }; }, }; From 6aec8ee3bd47c6b2fe4bb6b343da29e543b96e3e Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 10:26:44 +0900 Subject: [PATCH 093/281] =?UTF-8?q?[BE]=20FEAT:=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20API=20position=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/task.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index ec8d9e08..fb4de9c7 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -23,6 +23,7 @@ const getTaskById = asyncTryCatch(async (req, res) => { }); const createTask = asyncTryCatch(async (req, res) => { + const { projectId, sectionId } = req.params; const { labelIdList, dueDate, ...rest } = req.body; if (!isValidDueDate(dueDate)) { @@ -32,8 +33,19 @@ const createTask = asyncTryCatch(async (req, res) => { } await sequelize.transaction(async t => { - const task = await models.task.create({ dueDate, ...rest }, { transaction: t }); - await task.setLabels(JSON.parse(labelIdList), { transaction: t }); + const section = await models.section.findByPk(sectionId, { include: 'tasks' }); + + const maxPosition = section.toJSON().tasks.reduce((maxPosition, task) => { + return maxPosition < task.position ? task.position : maxPosition; + }, 0); + + const task = await models.task.create( + { projectId, sectionId, dueDate, position: maxPosition + 1, ...rest }, + { transaction: t }, + ); + if (labelIdList) { + await task.setLabels(JSON.parse(labelIdList), { transaction: t }); + } }); responseHandler(res, 201, { message: 'ok' }); From 2cda75532a88a0f2862484614378bed1fff434ac Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 10:28:31 +0900 Subject: [PATCH 094/281] =?UTF-8?q?[FE]=20FEAT:=20view=20container=20css?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/views/Home.vue | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 19f36630..4d3416c9 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,20 +1,15 @@ @@ -27,6 +22,14 @@ export default { components: { // Menu, }, - data: () => ({ drawer: null }), + data: () => ({ drawer: false }), }; + + From e9138d45f015798d461e6c886519530be4a4cda9 Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 10:29:15 +0900 Subject: [PATCH 095/281] =?UTF-8?q?[FE]=20FEAT:=20yyyy-mm-dd=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=98=A4=EB=8A=98=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/utils/today-string.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 client/src/utils/today-string.js diff --git a/client/src/utils/today-string.js b/client/src/utils/today-string.js new file mode 100644 index 00000000..3418eabb --- /dev/null +++ b/client/src/utils/today-string.js @@ -0,0 +1,10 @@ +const getTodayString = () => { + const today = new Date(); + const dd = String(today.getDate()).padStart(2, "0"); + const mm = String(today.getMonth() + 1).padStart(2, "0"); //January is 0! + const yyyy = today.getFullYear(); + + return `${yyyy}-${mm}-${dd}`; +}; + +export default getTodayString; From 2c4e0d825b53007c6a7067b0dfa1e75d1a0de833 Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 10:30:14 +0900 Subject: [PATCH 096/281] =?UTF-8?q?[FE]=20FEAT:=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EA=B8=B0=EB=8A=A5=20API=EC=99=80=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/task.js | 7 +- client/src/components/project/AddTask.vue | 95 +++++++++++++++++++---- client/src/store/project.js | 15 +++- 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/client/src/api/task.js b/client/src/api/task.js index c02b161a..16742174 100644 --- a/client/src/api/task.js +++ b/client/src/api/task.js @@ -1,9 +1,12 @@ import myAxios from "./myAxios"; -const projectAPI = { +const taskAPI = { + createTask({ projectId, sectionId, ...data }) { + return myAxios.POST(`/project/${projectId}/section/${sectionId}/task`, data); + }, updateTask(taskId, data) { return myAxios.PATCH(`/task/${taskId}`, data); }, }; -export default projectAPI; +export default taskAPI; diff --git a/client/src/components/project/AddTask.vue b/client/src/components/project/AddTask.vue index 6cd2b544..212b3749 100644 --- a/client/src/components/project/AddTask.vue +++ b/client/src/components/project/AddTask.vue @@ -1,30 +1,91 @@ - + diff --git a/client/src/store/project.js b/client/src/store/project.js index 45ac2573..c24436ea 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -34,7 +34,20 @@ const actions = { await dispatch("fetchCurrentProject", projectId); } catch (err) { - alert("프로젝트 조회 요청 실패"); + alert("프로젝트 수정 요청 실패"); + } + }, + async addTask({ dispatch }, task) { + try { + const { data } = await taskAPI.createTask(task); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", task.projectId); + } catch { + alert("프로젝트 추가 요청 실패"); } }, }; From 892e83d46a9010ce0dbfe8d56d0dca6bf60dcccf Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 10:31:07 +0900 Subject: [PATCH 097/281] =?UTF-8?q?[FE]=20REFACTOR:=20taks=20item=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20project=20conta?= =?UTF-8?q?iner=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=EB=B6=80?= =?UTF-8?q?=ED=84=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/project/TaskItem.vue | 36 +++++++++++ client/src/components/project/index.vue | 75 +++++++++++++--------- 2 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 client/src/components/project/TaskItem.vue diff --git a/client/src/components/project/TaskItem.vue b/client/src/components/project/TaskItem.vue new file mode 100644 index 00000000..f9319a9c --- /dev/null +++ b/client/src/components/project/TaskItem.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/client/src/components/project/index.vue b/client/src/components/project/index.vue index 8fa96667..83060fe0 100644 --- a/client/src/components/project/index.vue +++ b/client/src/components/project/index.vue @@ -1,49 +1,55 @@ From 8c6b3ef759af9758c2811da044250f9180c48eb3 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 2 Dec 2020 10:41:21 +0900 Subject: [PATCH 098/281] =?UTF-8?q?[BE]=20REFACTOR:=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=A9=94=ED=84=B0=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/task.js | 8 ++++---- server/src/passport/naver-strategy.js | 2 +- server/src/services/task.js | 10 ++++++---- server/src/services/user.js | 3 ++- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index e763003f..0263cf8f 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -16,7 +16,7 @@ const getAllTasks = asyncTryCatch(async (req, res) => { }); const createTask = asyncTryCatch(async (req, res) => { - const { labelIdList, dueDate, ...rest } = req.body; + const { dueDate } = req.body; // TODO middle ware로 빼내는게 좋을 것 같음 if (!isValidDueDate(dueDate)) { @@ -25,12 +25,12 @@ const createTask = asyncTryCatch(async (req, res) => { throw err; } - await taskService.create(labelIdList, dueDate, rest); + await taskService.create(req.body); responseHandler(res, 201, { message: 'ok' }); }); const updateTask = asyncTryCatch(async (req, res) => { - const { labelIdList, dueDate, ...rest } = req.body; + const { dueDate } = req.body; if (!isValidDueDate(dueDate)) { const err = new Error('유효하지 않은 dueDate'); @@ -40,7 +40,7 @@ const updateTask = asyncTryCatch(async (req, res) => { const { taskId } = req.params; - await taskService.update(labelIdList, dueDate, taskId, rest); + await taskService.update({ id: taskId, ...req.body }); responseHandler(res, 200, { message: 'ok' }); }); diff --git a/server/src/passport/naver-strategy.js b/server/src/passport/naver-strategy.js index 3791407c..4a88d291 100644 --- a/server/src/passport/naver-strategy.js +++ b/server/src/passport/naver-strategy.js @@ -11,7 +11,7 @@ const getNaverUser = async (accessToken, refreshToken, profile, done) => { const NAVER = 'naver'; try { const { email, nickname } = profile._json; - const [user] = await userService.retrieveOrCreate(email, nickname, NAVER); + const [user] = await userService.retrieveOrCreate({ email, nickname, provider: NAVER }); return done(null, user.toJSON()); } catch (err) { diff --git a/server/src/services/task.js b/server/src/services/task.js index f8b2754e..8373f0a0 100644 --- a/server/src/services/task.js +++ b/server/src/services/task.js @@ -34,9 +34,10 @@ const retrieveAll = async userId => { return tasks; }; -const create = async (labelIdList, dueDate, taskData) => { +const create = async taskData => { + const { labelIdList, dueDate, ...rest } = taskData; const result = await sequelize.transaction(async t => { - const task = await taskModel.create({ dueDate, ...taskData }, { transaction: t }); + const task = await taskModel.create({ dueDate, ...rest }, { transaction: t }); await task.setLabels(JSON.parse(labelIdList), { transaction: t }); return task; @@ -45,10 +46,11 @@ const create = async (labelIdList, dueDate, taskData) => { return !!result; }; -const update = async (labelIdList, dueDate, id, taskData) => { +const update = async taskData => { + const { id, labelIdList, dueDate, ...rest } = taskData; const result = await sequelize.transaction(async t => { await taskModel.update( - { dueDate, ...taskData }, + { dueDate, ...rest }, { where: { id }, }, diff --git a/server/src/services/user.js b/server/src/services/user.js index 1bdffd6d..a56b46d4 100644 --- a/server/src/services/user.js +++ b/server/src/services/user.js @@ -7,7 +7,8 @@ const retrieveById = async id => { return foundUser; }; -const retrieveOrCreate = async (email, name, provider) => { +const retrieveOrCreate = async userData => { + const { email, name, provider } = userData; const user = await userModel.findOrCreate({ where: { email, provider }, attributes: ['id', 'email', 'name', 'provider'], From 740190898c7ebaa145b844115f6d14cf89a65764 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 2 Dec 2020 11:03:59 +0900 Subject: [PATCH 099/281] =?UTF-8?q?[FE]=20FIX:=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/task.js | 2 +- client/src/components/task/Search.vue | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/api/task.js b/client/src/api/task.js index 8fb94d36..40eafaf1 100644 --- a/client/src/api/task.js +++ b/client/src/api/task.js @@ -1,7 +1,7 @@ import myAxios from "./myAxios"; const taskAPI = { - serachTask() { + searchTask() { return myAxios.GET("/task"); }, updateTask(taskId, data) { diff --git a/client/src/components/task/Search.vue b/client/src/components/task/Search.vue index 4c0077b5..1fe9535e 100644 --- a/client/src/components/task/Search.vue +++ b/client/src/components/task/Search.vue @@ -77,8 +77,7 @@ export default { this.isLoading = true; // Lazily load input items, - const res = await taskAPI.serachTask(); - + const res = await taskAPI.searchTask(); this.tasks = res.data.tasks; this.isLoading = false; }, From 0864f1a5cc6c944a669246e2cc77722a2569ce69 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 14:29:47 +0900 Subject: [PATCH 100/281] =?UTF-8?q?[FE]=20FEAT:=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - label api, store, labelList component 구현 --- client/src/api/label.js | 9 +++++++++ client/src/components/menu/LabelList.vue | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 client/src/api/label.js create mode 100644 client/src/components/menu/LabelList.vue diff --git a/client/src/api/label.js b/client/src/api/label.js new file mode 100644 index 00000000..4168b88b --- /dev/null +++ b/client/src/api/label.js @@ -0,0 +1,9 @@ +import myAxios from "./myAxios"; + +const labelAPI = { + getLabels() { + return myAxios.GET("/label"); + }, +}; + +export default labelAPI; diff --git a/client/src/components/menu/LabelList.vue b/client/src/components/menu/LabelList.vue new file mode 100644 index 00000000..c9d0f55b --- /dev/null +++ b/client/src/components/menu/LabelList.vue @@ -0,0 +1,22 @@ + + + From e8b32ef58f216abc86c33861e05f27cdce8eb97e Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 14:30:53 +0900 Subject: [PATCH 101/281] =?UTF-8?q?[FE]=20priority=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FilterList 컴포넌트 , priority store 구현 --- client/src/api/priority.js | 0 .../components/menu/FavoriteProjectList.vue | 22 +++++++-- client/src/components/menu/FilterList.vue | 22 +++++++++ client/src/components/menu/LeftMenu.vue | 29 ++++++------ .../components/menu/ProjectListContainer.vue | 22 +++++++++ client/src/store/index.js | 4 +- client/src/store/label.js | 33 +++++++++++++ client/src/store/priority.js | 46 +++++++++++++++++++ client/src/store/project.js | 10 ++-- 9 files changed, 162 insertions(+), 26 deletions(-) create mode 100644 client/src/api/priority.js create mode 100644 client/src/components/menu/FilterList.vue create mode 100644 client/src/components/menu/ProjectListContainer.vue create mode 100644 client/src/store/label.js create mode 100644 client/src/store/priority.js diff --git a/client/src/api/priority.js b/client/src/api/priority.js new file mode 100644 index 00000000..e69de29b diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index f968cb5f..c7983b5d 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -1,9 +1,21 @@ diff --git a/client/src/components/menu/LeftMenu.vue b/client/src/components/menu/LeftMenu.vue index a73fad5d..32674abc 100644 --- a/client/src/components/menu/LeftMenu.vue +++ b/client/src/components/menu/LeftMenu.vue @@ -1,35 +1,34 @@ diff --git a/client/src/components/menu/ProjectListContainer.vue b/client/src/components/menu/ProjectListContainer.vue new file mode 100644 index 00000000..ba18c919 --- /dev/null +++ b/client/src/components/menu/ProjectListContainer.vue @@ -0,0 +1,22 @@ + + + diff --git a/client/src/store/index.js b/client/src/store/index.js index d616f102..23c17378 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -2,9 +2,11 @@ import Vue from "vue"; import Vuex from "vuex"; import auth from "./auth"; import project from "./project"; +import label from "./label"; +import priority from "./priority"; Vue.use(Vuex); export default new Vuex.Store({ - modules: { auth, project }, + modules: { auth, project, label, priority }, }); diff --git a/client/src/store/label.js b/client/src/store/label.js new file mode 100644 index 00000000..39a63536 --- /dev/null +++ b/client/src/store/label.js @@ -0,0 +1,33 @@ +import labelAPI from "@/api/label"; + +const state = { + newLabel: {}, + labels: [], +}; + +const getters = { + labels: (state) => state.labels, +}; + +const mutations = { + SET_LABELS: (state, labels) => (state.labels = labels), +}; + +const actions = { + async fetchLabels({ commit }) { + try { + const { data: labels } = await labelAPI.getLabels(); + + commit("SET_LABELS", labels); + } catch (err) { + alert("라벨 전체 정보 조회 요청 실패"); + } + }, +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/store/priority.js b/client/src/store/priority.js new file mode 100644 index 00000000..f00c6513 --- /dev/null +++ b/client/src/store/priority.js @@ -0,0 +1,46 @@ +const state = { + priorities: [], +}; + +const getters = { + priorities: (state) => state.priorities, +}; + +const mutations = { + SET_PRIORITIES: (state, priorities) => (state.priorities = priorities), +}; + +const actions = { + async fetchPriorities({ commit }) { + try { + const priorities = [ + { + id: "c3bb8b39-cdad-4db4-ac02-ae506d30ba2a", + title: "우선순위1", + }, + { + id: "248d7dd6-9f9b-4bff-b47d-43a8b07c9093", + title: "우선순위2", + }, + { + id: "ac7ac13c-53df-49a3-8617-654e23f3d043", + title: "우선순위3", + }, + { + id: "936f1c1d-e169-47c4-b544-1f8a0aff0a8d", + title: "우선순위4", + }, + ]; + commit("SET_PRIORITIES", priorities); + } catch (err) { + alert("우선순위 전체 정보 조회 요청 실패"); + } + }, +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/store/project.js b/client/src/store/project.js index 898752f2..cef6f9e3 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -22,7 +22,7 @@ const actions = { try { const { data: project } = await projectAPI.getProjectById(projectId); - commit("setCurrentProject", project); + commit("SET_CURRENT_PROJECT", project); } catch (err) { alert("프로젝트 조회 요청 실패"); } @@ -42,9 +42,9 @@ const actions = { }, async fetchProjectInfos({ commit }) { try { - const { data: projects } = await projectAPI.getProjects(); + const { data: projectInfos } = await projectAPI.getProjects(); - commit("setProjects", projects); + commit("SET_PROJECT_INFOS", projectInfos); } catch (err) { alert("프로젝트 전체 정보 조회 요청 실패"); } @@ -53,8 +53,8 @@ const actions = { const mutations = { //TODO: function vs arrow-function style-guide 보고 통일하기 - setCurrentProject: (state, currentProject) => (state.currentProject = currentProject), - setProjects: (state, projects) => (state.projects = projects), + SET_CURRENT_PROJECT: (state, currentProject) => (state.currentProject = currentProject), + SET_PROJECT_INFOS: (state, projectInfos) => (state.projectInfos = projectInfos), // newTodo: (state, todo) => state.todos.unshift(todo), }; From 1d1317a89588e70c9d3c6eafa8f77fe5d36e82b5 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 14:31:27 +0900 Subject: [PATCH 102/281] =?UTF-8?q?[FE]=20FEAT:=20whaleGreen,=20whaleBlue?= =?UTF-8?q?=20=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/plugins/vuetify.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/plugins/vuetify.js b/client/src/plugins/vuetify.js index 2d5a28c6..0ead1cfa 100644 --- a/client/src/plugins/vuetify.js +++ b/client/src/plugins/vuetify.js @@ -7,8 +7,8 @@ export default new Vuetify({ theme: { themes: { light: { - whaleGreen: "#b7e1cd", - whaleBlue: "#161c70", + whaleGreen: "#07C4A3", + whaleBlue: "#1C2B82", }, }, }, From bf38037ca83fb031a457d7164a8e925f999a2fd0 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 2 Dec 2020 14:38:51 +0900 Subject: [PATCH 103/281] =?UTF-8?q?[BE]=20REFACTOR:=20project=20API=20test?= =?UTF-8?q?=20code=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/project.api.test.js | 346 +++++++++++++++----------------- 1 file changed, 158 insertions(+), 188 deletions(-) diff --git a/server/test/project.api.test.js b/server/test/project.api.test.js index 5c7af2f9..a9651d57 100644 --- a/server/test/project.api.test.js +++ b/server/test/project.api.test.js @@ -2,6 +2,8 @@ require('module-alias/register'); const request = require('supertest'); const app = require('@root/app'); const seeder = require('@test/test-seed'); +const status = require('@test/response-status'); +const { createJWT } = require('@utils/auth'); beforeAll(async done => { await seeder.up(); @@ -13,258 +15,226 @@ afterAll(async done => { done(); }); -const SUCCESS_CODE = 201; -const SUCCESS_MSG = 'ok'; - describe('get all projects', () => { - it('project get all 일반', done => { + it('project get all 일반', async done => { + // given + const expectedUser = seeder.users[0]; const expectedProjects = seeder.projects.map(project => { - const tasks = seeder.tasks.filter(task => task.projectId === project.id); + const tasks = seeder.tasks.filter( + task => project.creatorId === expectedUser.id && task.projectId === project.id, + ); const { id, title } = project; return { id, title, taskCount: tasks.length }; }); - // [ - // { id: 'b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f', taskCount: 5, title: '프로젝트 1' }, - // { id: 'f7605077-96ec-4365-88fc-a9c3af4a084e', taskCount: 0, title: '프로젝트 2' }, - // { taskCount: 2, title: '오늘' }, - // ]; - - try { - request(app) - .get('/api/project') - .end((err, res) => { - if (err) { - throw err; - } - expect( - res.body.every(project => - expectedProjects.some( - expectedProject => - Object.entries(project).toString === Object.entries(expectedProject).toString, - ), - ), - ).toBeTruthy(); - - done(); - }); - } catch (err) { - done(err); - } + + // when + const res = await request(app) + .get('/api/project') + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + const recievedProjects = res.body; + + // then + expect( + recievedProjects.every(project => + expectedProjects.some( + expectedProject => + Object.entries(project).toString === Object.entries(expectedProject).toString, + ), + ), + ).toBeTruthy(); + + done(); }); }); describe('get project by id', () => { - it('project get by id 일반', done => { - const expectedChildTaskId = '8d62f93c-9233-46a9-a5cf-ec18ad5a36f4'; - - try { - request(app) - .get('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f') - .end((err, res) => { - if (err) { - throw err; - } - const childTask = res.body.sections[0].tasks[0].tasks[0]; - expect(childTask.id).toEqual(expectedChildTaskId); - done(); - }); - } catch (err) { - done(err); - } + it('project get by id 일반', async done => { + // given + const expectedChildTaskId = seeder.tasks[3].id; + const expectedProjectId = seeder.projects[0].id; + const expectedUser = seeder.users[0]; + + // when + const res = await request(app) + .get(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + + const childTask = res.body.sections[0].tasks[0].tasks[0]; + + // then + expect(childTask.id).toEqual(expectedChildTaskId); + done(); }); }); describe('create project', () => { - it('create project 일반', done => { + it('create project 일반', async done => { + // given const requestBody = { title: '새 프로젝트', isList: true, }; - - try { - request(app) - .post('/api/project/') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + const expectedUser = seeder.users[0]; + + // when + const res = await request(app) + .post('/api/project/') + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('update project', () => { - it('update project PUT', done => { + it('update project PUT', async done => { + // given + const expectedProjectId = seeder.projects[0].id; const requestBody = { title: '변경된 프로젝트', isList: true, isFavorite: true, }; - - try { - request(app) - .put('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + const expectedUser = seeder.users[0]; + + // when + const res = await request(app) + .put(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); - it('update project PATCH', done => { + it('update project PATCH', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; const requestBody = { title: '변경된 프로젝트', isList: true, isFavorite: true, }; - try { - request(app) - .patch('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .patch(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('delete project', () => { - it('delete project 일반', done => { - try { - request(app) - .delete('/api/project/f7605077-96ec-4365-88fc-a9c3af4a084e') - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + it('delete project 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[1]; + // when + const res = await request(app) + .delete(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('create section', () => { - it('create project 일반', done => { + it('create project 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; const requestBody = { title: '새로운 섹션', }; - try { - request(app) - .post('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('update section task positions', () => { - it('update section task positions 일반', done => { + it('update section task positions 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; const requestBody = { - orderedTasks: [ - '7d62f93c-9233-46a9-a5cf-ec18ad5a36f4', - 'cd62f93c-9233-46a9-a5cf-ec18ad5a36f4', - '13502adf-83dd-4e8e-9acf-5c5a0abd5b1b', - ], + orderedTasks: [seeder.tasks[2].id, seeder.tasks[1].id, seeder.tasks[0].id], }; - try { - request(app) - .post( - '/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section/7abf0633-bce2-4972-9249-69f287db8a47/task', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('update section', () => { - it('update section 일반', done => { + it('update section 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; const requestBody = { title: '바뀐 섹션', }; - try { - request(app) - .put( - '/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section/7abf0633-bce2-4972-9249-69f287db8a47', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .put(`/api/project/${expectedProjectId}/section/${expectedSectionId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('delete section', () => { - it('update section 일반', done => { - try { - request(app) - .delete( - '/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section/7abf0633-bce2-4972-9249-69f287db8a47', - ) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + it('update section 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + + // when + const res = await request(app) + .delete(`/api/project/${expectedProjectId}/section/${expectedSectionId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); From 5285ebb95180fe3801a7fb60bfac77e8c43d62b0 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 15:01:08 +0900 Subject: [PATCH 104/281] =?UTF-8?q?[FE]=20FEAT:=20=EC=98=A4=EB=8A=98=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=ED=8C=90=EB=B3=84=ED=95=98=EB=8A=94=20uti?= =?UTF-8?q?l=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/utils/date.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 client/src/utils/date.js diff --git a/client/src/utils/date.js b/client/src/utils/date.js new file mode 100644 index 00000000..c8130cc3 --- /dev/null +++ b/client/src/utils/date.js @@ -0,0 +1,13 @@ +const isToday = (inputDate) => { + const today = new Date(Date.now()); + + const targetDate = new Date(inputDate); + + return ( + today.getFullYear() === targetDate.getFullYear() && + today.getMonth() === targetDate.getMonth() && + today.getDate() === targetDate.getDate() + ); +}; + +export default { isToday }; From 6f2d832fd3ea0125e1b59385936d599a9a5e5ab2 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 15:01:41 +0900 Subject: [PATCH 105/281] =?UTF-8?q?[FE]=20todayTaskCount,=20nextDayTaskCou?= =?UTF-8?q?nt=20getters=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/menu/FavoriteProjectList.vue | 13 +++++++++---- client/src/components/menu/LeftMenu.vue | 16 +++++++++++++--- client/src/store/index.js | 3 ++- client/src/store/task.js | 9 +++++---- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index c7983b5d..62467f53 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -2,22 +2,27 @@
- 관리함 + 관리함 - 오늘 + 오늘 {{ todayTaskCount }} - 다음 + 다음 {{ nextDayTaskCount }}
diff --git a/client/src/components/menu/LeftMenu.vue b/client/src/components/menu/LeftMenu.vue index 32674abc..05a6b2b7 100644 --- a/client/src/components/menu/LeftMenu.vue +++ b/client/src/components/menu/LeftMenu.vue @@ -1,6 +1,9 @@ @@ -42,13 +57,26 @@ import { mapGetters, mapActions } from "vuex"; import AddTask from "./AddTask"; import TaskItem from "./TaskItem"; +import UpdatableTitle from "../common/UpdatableTitle"; +// import router from "@/router/index.js"; export default { + data() { + return { + dialog: false, + }; + }, methods: { ...mapActions(["fetchCurrentProject", "updateTaskToDone"]), + popTaskDetail(task) { + console.log(task); + this.dialog = true; + this.$router.push(`/task/${task.id}`); + // router.push(`/task/${obj.id}`).catch(() => {}); + }, }, computed: mapGetters(["currentProject"]), - components: { AddTask, TaskItem }, + components: { AddTask, TaskItem, UpdatableTitle }, created() { this.fetchCurrentProject(this.$route.params.projectId); }, diff --git a/client/src/components/project/index.vue b/client/src/components/project/index.vue deleted file mode 100644 index 83060fe0..00000000 --- a/client/src/components/project/index.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - - - diff --git a/client/src/store/project.js b/client/src/store/project.js index 937eeb27..3034b17e 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -27,6 +27,35 @@ const actions = { alert("프로젝트 조회 요청 실패"); } }, + + async updateProjectTitle({ dispatch }, { projectId, title }) { + try { + const { data } = await projectAPI.updateProject(projectId, { title }); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", projectId); + } catch (err) { + alert("프로젝트 수정 요청 실패"); + } + }, + + async updateSectionTitle({ dispatch }, { projectId, sectionId, title }) { + try { + const { data } = await projectAPI.updateSection(projectId, sectionId, { title }); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", projectId); + } catch (err) { + alert("섹션 수정 요청 실패"); + } + }, + async updateTaskToDone({ dispatch }, { projectId, taskId }) { try { const { data } = await taskAPI.updateTask(taskId, { isDone: true }); From 1e4034df845b626d1f7c944224507fca6af8932b Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 21:51:16 +0900 Subject: [PATCH 119/281] =?UTF-8?q?[FE]=20task=20detail=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B4=88=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/TaskDetail.vue | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 client/src/components/task/TaskDetail.vue diff --git a/client/src/components/task/TaskDetail.vue b/client/src/components/task/TaskDetail.vue new file mode 100644 index 00000000..a3375195 --- /dev/null +++ b/client/src/components/task/TaskDetail.vue @@ -0,0 +1,9 @@ + + + + + From 80c0520ae4fbfdecc91c391d9bc7319137340922 Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 21:52:04 +0900 Subject: [PATCH 120/281] =?UTF-8?q?[BE]=20=EC=9E=91=EC=97=85=EC=9D=98=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=EC=9E=91=EC=97=85=EB=8F=84=20isDone=3Dfalse?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=83=EB=93=A4=EB=A7=8C=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EB=8F=84=EB=A1=9D=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20API=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/project.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/src/controllers/project.js b/server/src/controllers/project.js index c9e1fdd8..4b825eeb 100644 --- a/server/src/controllers/project.js +++ b/server/src/controllers/project.js @@ -44,7 +44,16 @@ const getProjectById = asyncTryCatch(async (req, res) => { include: { model: models.task, where: { isDone: false, parentId: null }, - include: ['priority', 'labels', 'alarm', 'tasks'], + include: [ + 'priority', + 'labels', + 'alarm', + { + model: models.task, + where: { isDone: false }, + required: false, + }, + ], required: false, }, }, From bfcb2c4b5643706cedb0f2fc3dc1c944871560b2 Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 2 Dec 2020 21:52:58 +0900 Subject: [PATCH 121/281] =?UTF-8?q?[FE]=20=EC=9E=91=EC=97=85=EC=9D=98=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=EC=8B=9C=20=EC=9E=91=EC=97=85=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=AA=A8=EB=8B=AC=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/router/index.js | 24 +++++++++++++----------- client/src/views/Project.vue | 5 ++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/src/router/index.js b/client/src/router/index.js index fda4a0dd..4928d592 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -1,11 +1,12 @@ import Vue from "vue"; import VueRouter from "vue-router"; -import Login from "../views/Login.vue"; -import Today from "../views/Today.vue"; -import Project from "../views/Project.vue"; -import Task from "../views/Task.vue"; -import Home from "../views/Home.vue"; +import Login from "@/views/Login.vue"; +import Today from "@/views/Today.vue"; +import Project from "@/views/Project.vue"; +import Task from "@/views/Task.vue"; +import Home from "@/views/Home.vue"; import userAPI from "@/api/user"; +import TaskDetail from "@/components/task/TaskDetail"; Vue.use(VueRouter); @@ -51,12 +52,13 @@ const routes = [ name: "Project", component: Project, beforeEnter: requireAuth(), - }, - { - path: "task/:taskId", - name: "Task", - component: Task, - beforeEnter: requireAuth(), + chiledren: [ + { + path: "../task/:taskId", + name: "TaskDetail", + component: TaskDetail, + }, + ], }, ], }, diff --git a/client/src/views/Project.vue b/client/src/views/Project.vue index 257112d7..f50f5dfe 100644 --- a/client/src/views/Project.vue +++ b/client/src/views/Project.vue @@ -3,10 +3,9 @@ From b4c3d644d733d63fb9c8fc954bc27c983ce678cf Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 2 Dec 2020 22:21:07 +0900 Subject: [PATCH 122/281] =?UTF-8?q?[FE]=20FEAT:=20Header=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 색깔 변경, 패딩 변경 --- client/src/components/common/Header.vue | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/client/src/components/common/Header.vue b/client/src/components/common/Header.vue index 7db7106d..87913961 100644 --- a/client/src/components/common/Header.vue +++ b/client/src/components/common/Header.vue @@ -1,13 +1,13 @@ diff --git a/client/src/components/task/TaskDetail.vue b/client/src/components/task/TaskDetail.vue index 3aa6532b..29574bb0 100644 --- a/client/src/components/task/TaskDetail.vue +++ b/client/src/components/task/TaskDetail.vue @@ -1,8 +1,6 @@ diff --git a/client/src/views/Project.vue b/client/src/views/Project.vue index f50f5dfe..9570b1e4 100644 --- a/client/src/views/Project.vue +++ b/client/src/views/Project.vue @@ -1,11 +1,22 @@ From 3d85fa12e13fba81f8f3aa7aef218eefc276a7d7 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Thu, 3 Dec 2020 04:40:56 +0900 Subject: [PATCH 139/281] =?UTF-8?q?[FE]=20FIX:=20todayProject=20id=20?= =?UTF-8?q?=EC=A7=80=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/menu/FavoriteProjectList.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index ed70b046..63a306d5 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -15,7 +15,7 @@ mdi-calendar-today - + 오늘 {{ todayProject.taskCount }} @@ -26,7 +26,6 @@ diff --git a/client/src/components/menu/LeftMenu.vue b/client/src/components/menu/LeftMenu.vue index 81e890c3..fc95afb4 100644 --- a/client/src/components/menu/LeftMenu.vue +++ b/client/src/components/menu/LeftMenu.vue @@ -3,7 +3,7 @@ @@ -32,11 +32,20 @@ export default { "priorities", "todayProject", "nextDayTaskCount", + "taskCount", + "todayTasks", ]), methods: { - ...mapActions(["fetchProjectInfos", "fetchTodayProject", "fetchLabels", "fetchPriorities"]), + ...mapActions([ + "fetchProjectInfos", + "fetchTodayProject", + "fetchLabels", + "fetchPriorities", + "fetchAllTasks", + ]), }, created() { + this.fetchAllTasks(); this.fetchProjectInfos(); this.fetchTodayProject(); this.fetchLabels(); From 8d916b60d57b80ec0519d24f13591e4904e7dd86 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Thu, 3 Dec 2020 05:20:31 +0900 Subject: [PATCH 144/281] =?UTF-8?q?[FE]=20FEAT:=20TodayTasksContainer=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20today=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=8C=80=EB=9E=B5=EC=A0=81=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getter로 받아준 expiredTasks와 todayTasks를 활용 - Today.vue에서 props로 넘겨주기 --- client/src/components/menu/index.vue | 19 ------ .../components/today/TodayTasksContainer.vue | 62 +++++++++++++++++++ client/src/views/Today.vue | 12 +++- 3 files changed, 73 insertions(+), 20 deletions(-) delete mode 100644 client/src/components/menu/index.vue create mode 100644 client/src/components/today/TodayTasksContainer.vue diff --git a/client/src/components/menu/index.vue b/client/src/components/menu/index.vue deleted file mode 100644 index 1890b195..00000000 --- a/client/src/components/menu/index.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/client/src/components/today/TodayTasksContainer.vue b/client/src/components/today/TodayTasksContainer.vue new file mode 100644 index 00000000..19964410 --- /dev/null +++ b/client/src/components/today/TodayTasksContainer.vue @@ -0,0 +1,62 @@ + + + diff --git a/client/src/views/Today.vue b/client/src/views/Today.vue index 71fa24ec..55eaf334 100644 --- a/client/src/views/Today.vue +++ b/client/src/views/Today.vue @@ -1,11 +1,21 @@ From 392bd2f268e4cd9c213dc36e8d94637d23df75f0 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Thu, 3 Dec 2020 11:40:49 +0900 Subject: [PATCH 145/281] =?UTF-8?q?[FE]=20FEAT:=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20URL=EB=A1=9C=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EC=98=A8=20=EA=B2=BD=EC=9A=B0,=20dialog=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/project/ProjectContainer.vue | 10 ++-------- client/src/components/task/TaskDetail.vue | 3 --- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/client/src/components/project/ProjectContainer.vue b/client/src/components/project/ProjectContainer.vue index b0f8d1cc..4e1f4821 100644 --- a/client/src/components/project/ProjectContainer.vue +++ b/client/src/components/project/ProjectContainer.vue @@ -43,15 +43,9 @@ - + - Open Dialog @@ -65,7 +59,7 @@ import router from "@/router"; export default { data() { return { - dialog: false, + dialog: !!this.$route.params.taskId, projectId: this.$route.params.projectId, }; }, diff --git a/client/src/components/task/TaskDetail.vue b/client/src/components/task/TaskDetail.vue index 29574bb0..c524a757 100644 --- a/client/src/components/task/TaskDetail.vue +++ b/client/src/components/task/TaskDetail.vue @@ -18,9 +18,6 @@ export default { ...mapActions(["fetchCurrentTask"]), }, computed: mapGetters(["currentTask"]), - mounted() { - this.$emit("setDialog"); - }, created() { this.fetchCurrentTask(this.$route.params.taskId); }, From 2d430fd2e8b2e4a811d0fe58b23e138469feaf30 Mon Sep 17 00:00:00 2001 From: shkilo Date: Thu, 3 Dec 2020 13:36:16 +0900 Subject: [PATCH 146/281] =?UTF-8?q?[BE]=20FIX:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20API=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/routes/project.js | 2 +- server/src/services/section.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/routes/project.js b/server/src/routes/project.js index f8e9541a..e1ccff5b 100644 --- a/server/src/routes/project.js +++ b/server/src/routes/project.js @@ -15,7 +15,7 @@ router.delete('/:projectId', projectController.deleteProject); router.post('/:projectId/section', sectionController.createSection); router.put('/:projectId/section/:sectionId', sectionController.updateSection); router.delete('/:projectId/section/:sectionId', sectionController.deleteSection); -router.post('/:projectId/section/:sectionId/task/position', sectionController.updateTaskPositions); +router.patch('/:projectId/section/:sectionId/position', sectionController.updateTaskPositions); router.post('/:projectId/section/:sectionId/task', taskController.createTask); diff --git a/server/src/services/section.js b/server/src/services/section.js index c238ac30..c7f6ca09 100644 --- a/server/src/services/section.js +++ b/server/src/services/section.js @@ -17,10 +17,10 @@ const create = async ({ projectId, ...data }) => { return !!result; }; -const update = async (id, ...data) => { - const result = await models.section.update(data, { where: { id } }); +const update = async ({ id, ...data }) => { + const [result] = await models.section.update(data, { where: { id } }); - return result.length !== 0; + return result !== 0; }; const updateTaskPositions = async orderedTasks => { From 649cd30839b72bd6f438bd6ed070cee2bf348b02 Mon Sep 17 00:00:00 2001 From: shkilo Date: Thu, 3 Dec 2020 13:42:29 +0900 Subject: [PATCH 147/281] =?UTF-8?q?[FE]=20FEAT:=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=EC=95=A4=20=EB=93=9C=EB=9E=8D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TaskItem 에 드래그 이벤트및 이벤트 핸들러 등록 - store에 드래그앤 드랍 해당 action 구현 - 동작은 되나 순서가 뒤죽박죽 됨 - section 컴포넌트를 나누고 그 안에서 순서 상태관리를 하도록 리팩토링 해야 함 --- client/src/api/project.js | 3 + .../components/project/ProjectContainer.vue | 15 +--- client/src/components/project/TaskItem.vue | 90 ++++++++++++++++--- client/src/store/project.js | 30 +++++++ client/src/store/task.js | 7 ++ client/src/utils/array-index-swap.js | 7 ++ 6 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 client/src/utils/array-index-swap.js diff --git a/client/src/api/project.js b/client/src/api/project.js index adee6933..c6ed5e99 100644 --- a/client/src/api/project.js +++ b/client/src/api/project.js @@ -13,6 +13,9 @@ const projectAPI = { updateSection(projectId, sectionId, data) { return myAxios.PUT(`/project/${projectId}/section/${sectionId}`, data); }, + updateTaskPosition(projectId, sectionId, data) { + return myAxios.PATCH(`/project/${projectId}/section/${sectionId}/position`, data); + }, }; export default projectAPI; diff --git a/client/src/components/project/ProjectContainer.vue b/client/src/components/project/ProjectContainer.vue index 6049ed93..599657ba 100644 --- a/client/src/components/project/ProjectContainer.vue +++ b/client/src/components/project/ProjectContainer.vue @@ -31,10 +31,8 @@ -
-
- -
+
+ @@ -46,7 +44,6 @@ Open Dialog - @@ -68,15 +65,9 @@ export default { }, methods: { ...mapActions(["fetchCurrentProject", "updateTaskToDone"]), - popTaskDetail(task) { - console.log(task); - this.dialog = true; - this.$router.push(`/task/${task.id}`); - // router.push(`/task/${obj.id}`).catch(() => {}); - }, }, computed: mapGetters(["currentProject"]), - components: { AddTask, TaskItem, UpdatableTitle }, + components: { AddTask, TaskItem, UpdatableTitle, TaskDetail }, created() { this.fetchCurrentProject(this.$route.params.projectId); }, diff --git a/client/src/components/project/TaskItem.vue b/client/src/components/project/TaskItem.vue index f9319a9c..5b4a13b4 100644 --- a/client/src/components/project/TaskItem.vue +++ b/client/src/components/project/TaskItem.vue @@ -1,27 +1,85 @@ @@ -33,4 +91,16 @@ export default { .done-checkbox { border-radius: 100%; } + +.done-checkbox:hover { + border-radius: 50%; +} + +.dragging { + opacity: 0.3; +} + +.draggedOver { + border-bottom: 2px solid blue !important; +} diff --git a/client/src/store/project.js b/client/src/store/project.js index 3034b17e..2e0ed815 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -1,5 +1,6 @@ import projectAPI from "../api/project"; import taskAPI from "../api/task"; +import swapArrayItem from "../utils/array-index-swap"; const state = { currentProject: { @@ -69,6 +70,7 @@ const actions = { alert("프로젝트 수정 요청 실패"); } }, + async addTask({ dispatch }, task) { try { const { data } = await taskAPI.createTask(task); @@ -82,6 +84,7 @@ const actions = { alert("프로젝트 추가 요청 실패"); } }, + async fetchProjectInfos({ commit }) { try { const { data: projectInfos } = await projectAPI.getProjects(); @@ -91,6 +94,33 @@ const actions = { alert("프로젝트 전체 정보 조회 요청 실패"); } }, + + async changeTaskPosition({ dispatch, rootState }, { section, task }) { + const taskIds = section.tasks.map((task) => task.id); + const draggingTask = rootState.task.draggingTask; + console.log(task.position); + if (task.sectionId === draggingTask.sectionId) { + swapArrayItem(taskIds, draggingTask.position, task.position); + } else { + taskIds.splice(task.position + 1, 0, task.id); + } + + try { + await taskAPI.updateTask(draggingTask.id, { sectionId: section.id }); + + const { data } = await projectAPI.updateTaskPosition(section.projectId, section.id, { + orderedTasks: taskIds, + }); + + if (data.message !== "ok") { + throw new Error(); + } + } catch (err) { + alert("위치 변경 실패"); + } + + await dispatch("fetchCurrentProject", section.projectId); + }, }; const mutations = { diff --git a/client/src/store/task.js b/client/src/store/task.js index cbec6520..62e38455 100644 --- a/client/src/store/task.js +++ b/client/src/store/task.js @@ -4,11 +4,13 @@ import { isToday } from "@/utils/date"; const state = { newTask: {}, tasks: [], + draggingTask: {}, }; const getters = { todayTaskCount: (state) => state.tasks.filter((task) => isToday(task.dueDate)).length, nextDayTaskCount: (state) => state.tasks.filter((task) => !isToday(task.dueDate)).length, + draggingTask: (state) => state.draggingTask, }; const actions = { @@ -20,10 +22,15 @@ const actions = { alert("작업 전체 조회 요청 실패"); } }, + + startDragTask({ commit }, { task }) { + commit("SET_DRAGGING_TASK", task); + }, }; const mutations = { SET_TASKS: (state, tasks) => (state.tasks = tasks), + SET_DRAGGING_TASK: (state, task) => (state.draggingTask = task), }; export default { diff --git a/client/src/utils/array-index-swap.js b/client/src/utils/array-index-swap.js new file mode 100644 index 00000000..c3432141 --- /dev/null +++ b/client/src/utils/array-index-swap.js @@ -0,0 +1,7 @@ +const swapArrayItem = (arr, i, j) => { + const copied = [...arr][j]; + arr[j] = arr[i]; + arr[i] = copied; +}; + +export default swapArrayItem; From 592144bdccdaeca363eefcdbb4eded69ca2b7491 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Thu, 3 Dec 2020 22:30:33 +0900 Subject: [PATCH 148/281] =?UTF-8?q?[FE]=20FEAT:=20beforeRouteUpdate=20rout?= =?UTF-8?q?e=20guard=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/views/Project.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/views/Project.vue b/client/src/views/Project.vue index 9570b1e4..83ef7584 100644 --- a/client/src/views/Project.vue +++ b/client/src/views/Project.vue @@ -4,7 +4,7 @@ From b31463ab32c319edbfda5b168fced56610b83078 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Thu, 3 Dec 2020 23:52:40 +0900 Subject: [PATCH 149/281] =?UTF-8?q?[FE]=20REFACTOR:=20requireAuth=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 매번 라우팅할때마다 인증 API 요청을 날리는 것은 overhead라고 판단 - 초기 렌더링 시 한번 인증 API를 날리기 떄문에 그 이후에서는 token 유무로 판단 - API 요청시 401 에러가 오면 login으로 가는 로직 만들 것 --- client/src/router/index.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/client/src/router/index.js b/client/src/router/index.js index ef8c3297..26588bff 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -3,21 +3,17 @@ import VueRouter from "vue-router"; import Login from "@/views/Login.vue"; import Today from "@/views/Today.vue"; import Project from "@/views/Project.vue"; -import Task from "@/views/Task.vue"; import Home from "@/views/Home.vue"; import userAPI from "@/api/user"; import TaskDetail from "@/components/task/TaskDetail.vue"; Vue.use(VueRouter); -// TODO: user/me api 2번 호출하는 문제 해결 -const requireAuth = () => async (from, to, next) => { - try { - await userAPI.authorize(); +const requireAuth = () => (from, to, next) => { + if (localStorage.getItem("token")) { return next(); - } catch (err) { - return next("/login"); } + return next("/login"); }; const redirectHome = () => async (from, to, next) => { From 816c1678906f8a808fd23cd190ee5461d8370971 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Fri, 4 Dec 2020 00:01:51 +0900 Subject: [PATCH 150/281] =?UTF-8?q?[FE]=20REFACTOR:=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=EA=B0=80=20modal=20=EC=9E=90=EC=B2=B4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/project/ProjectContainer.vue | 24 +++++++------------ client/src/components/project/TaskItem.vue | 24 ++++++++++++------- client/src/components/task/TaskDetail.vue | 19 +++++++++++++-- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/client/src/components/project/ProjectContainer.vue b/client/src/components/project/ProjectContainer.vue index 68d8a51a..a3a3a4ba 100644 --- a/client/src/components/project/ProjectContainer.vue +++ b/client/src/components/project/ProjectContainer.vue @@ -32,7 +32,7 @@
- + @@ -43,18 +43,14 @@ - - -
From f93450d62fa932729e8858fb53c55c4ef4ccb347 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Fri, 4 Dec 2020 00:28:42 +0900 Subject: [PATCH 151/281] =?UTF-8?q?[FE]=20FEAT:=20Today=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8F=84=20task=20=EC=83=81=EC=84=B8=EB=A1=9C=20=EB=84=98?= =?UTF-8?q?=EC=96=B4=EA=B0=80=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/project/TaskItem.vue | 15 ++++++++------- .../src/components/today/TodayTasksContainer.vue | 12 ++---------- client/src/router/index.js | 9 ++++++++- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/client/src/components/project/TaskItem.vue b/client/src/components/project/TaskItem.vue index febc24a3..ef49929f 100644 --- a/client/src/components/project/TaskItem.vue +++ b/client/src/components/project/TaskItem.vue @@ -46,13 +46,14 @@ export default { ...mapActions(["updateTaskToDone", "startDragTask", "changeTaskPosition"]), moveToTaskDetail() { - console.log(this.task); - this.$router - .push({ - name: "TaskDetail", - params: { projectId: this.task.projectId, taskId: this.task.id }, - }) - .catch(() => {}); + const destinationInfo = this.$route.params.projectId + ? { + name: "ProjectTaskDetail", + params: { projectId: this.task.projectId, taskId: this.task.id }, + } + : { name: "TodayTaskDetail", params: { taskId: this.task.id } }; + + this.$router.push(destinationInfo).catch(() => {}); }, handleDragStart() { this.dragging = true; diff --git a/client/src/components/today/TodayTasksContainer.vue b/client/src/components/today/TodayTasksContainer.vue index 19964410..bc4c6946 100644 --- a/client/src/components/today/TodayTasksContainer.vue +++ b/client/src/components/today/TodayTasksContainer.vue @@ -4,9 +4,7 @@ 기한이 지난
-
- -
+ @@ -21,9 +19,7 @@ 오늘
-
- -
+ @@ -52,10 +48,6 @@ export default { }, methods: { ...mapActions(["updateTaskToDone"]), - popTaskDetail(task) { - this.$router.push(`/task/${task.id}`); - // router.push(`/task/${obj.id}`).catch(() => {}); - }, }, components: { TaskItem }, }; diff --git a/client/src/router/index.js b/client/src/router/index.js index ef8c3297..a7bdd9f6 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -46,6 +46,13 @@ const routes = [ name: "Today", component: Today, beforeEnter: requireAuth(), + children: [ + { + path: "task/:taskId", + name: "TodayTaskDetail", + component: TaskDetail, + }, + ], }, { path: "project/:projectId", @@ -55,7 +62,7 @@ const routes = [ children: [ { path: "task/:taskId", - name: "TaskDetail", + name: "ProjectTaskDetail", component: TaskDetail, }, ], From af23553812e26ceb614c414350c1e3136f21af5f Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Fri, 4 Dec 2020 01:07:44 +0900 Subject: [PATCH 152/281] =?UTF-8?q?[FE]=20REFACTOR:=20axios=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - baseURL 로 axios 인스턴스 생성 - interceptor로 request 이전에 token을 header로 설정 --- client/src/api/label.js | 2 +- client/src/api/myAxios.js | 39 ++++++++++----------------------------- client/src/api/project.js | 12 ++++++------ client/src/api/task.js | 8 ++++---- client/src/api/user.js | 2 +- 5 files changed, 22 insertions(+), 41 deletions(-) diff --git a/client/src/api/label.js b/client/src/api/label.js index 4168b88b..e0713d27 100644 --- a/client/src/api/label.js +++ b/client/src/api/label.js @@ -2,7 +2,7 @@ import myAxios from "./myAxios"; const labelAPI = { getLabels() { - return myAxios.GET("/label"); + return myAxios.get("/label"); }, }; diff --git a/client/src/api/myAxios.js b/client/src/api/myAxios.js index 5c133d0e..be503f09 100644 --- a/client/src/api/myAxios.js +++ b/client/src/api/myAxios.js @@ -1,33 +1,14 @@ import axios from "axios"; -const getToken = () => localStorage.getItem("token"); - -const headerConfig = () => { - return { - headers: { - Authorization: "Bearer " + getToken(), - }, - }; -}; - -const SERVER_URL = process.env.VUE_APP_SERVER_URL + "/api"; - -const myAxios = { - GET: (path) => { - return axios.get(SERVER_URL + path, headerConfig()); - }, - POST: (path, body) => { - return axios.post(SERVER_URL + path, body, headerConfig()); - }, - PATCH: (path, body) => { - return axios.patch(SERVER_URL + path, body, headerConfig()); - }, - PUT: (path, body) => { - return axios.put(SERVER_URL + path, body, headerConfig()); - }, - DELETE: (path) => { - return axios.delete(SERVER_URL + path, headerConfig()); - }, -}; +const baseURL = process.env.VUE_APP_SERVER_URL + "/api"; +// axios intercept 전역 설정 +const myAxios = axios.create({ + baseURL, +}); + +myAxios.interceptors.request.use((config) => { + config.headers.Authorization = "Bearer " + localStorage.getItem("token"); + return config; +}); export default myAxios; diff --git a/client/src/api/project.js b/client/src/api/project.js index 36968a02..a53ef14e 100644 --- a/client/src/api/project.js +++ b/client/src/api/project.js @@ -2,22 +2,22 @@ import myAxios from "./myAxios"; const projectAPI = { getProjectById(projectId) { - return myAxios.GET(`/project/${projectId}`); + return myAxios.get(`/project/${projectId}`); }, getProjects() { - return myAxios.GET("/project"); + return myAxios.get("/project"); }, getTodayProject() { - return myAxios.GET("/project/today"); + return myAxios.get("/project/today"); }, updateProject(projectId, data) { - return myAxios.PATCH(`/project/${projectId}`, data); + return myAxios.patch(`/project/${projectId}`, data); }, updateSection(projectId, sectionId, data) { - return myAxios.PUT(`/project/${projectId}/section/${sectionId}`, data); + return myAxios.put(`/project/${projectId}/section/${sectionId}`, data); }, updateTaskPosition(projectId, sectionId, data) { - return myAxios.PATCH(`/project/${projectId}/section/${sectionId}/position`, data); + return myAxios.patch(`/project/${projectId}/section/${sectionId}/position`, data); }, }; diff --git a/client/src/api/task.js b/client/src/api/task.js index 02d6831b..af554823 100644 --- a/client/src/api/task.js +++ b/client/src/api/task.js @@ -2,16 +2,16 @@ import myAxios from "./myAxios"; const taskAPI = { createTask({ projectId, sectionId, ...data }) { - return myAxios.POST(`/project/${projectId}/section/${sectionId}/task`, data); + return myAxios.post(`/project/${projectId}/section/${sectionId}/task`, data); }, getAllTasks() { - return myAxios.GET("/task"); + return myAxios.get("/task"); }, getTaskById(taskId) { - return myAxios.GET(`/task/${taskId}`); + return myAxios.get(`/task/${taskId}`); }, updateTask(taskId, data) { - return myAxios.PATCH(`/task/${taskId}`, data); + return myAxios.patch(`/task/${taskId}`, data); }, }; diff --git a/client/src/api/user.js b/client/src/api/user.js index 33755579..ba0ace8b 100644 --- a/client/src/api/user.js +++ b/client/src/api/user.js @@ -2,7 +2,7 @@ import myAxios from "./myAxios"; const userAPI = { authorize() { - return myAxios.GET("/user/me"); + return myAxios.get("/user/me"); }, }; From f0e373605f485e0bb0cca9b89a40bde5176190a2 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Fri, 4 Dec 2020 03:20:32 +0900 Subject: [PATCH 153/281] =?UTF-8?q?[FE]=20FEAT:=20Task=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20task=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=83=81=EC=9C=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/TaskDetail.vue | 55 +++++++++++------------ client/src/router/index.js | 6 +-- client/src/views/Task.vue | 45 +++++++++++++++++-- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/client/src/components/task/TaskDetail.vue b/client/src/components/task/TaskDetail.vue index 3417b5c0..30b61169 100644 --- a/client/src/components/task/TaskDetail.vue +++ b/client/src/components/task/TaskDetail.vue @@ -1,42 +1,41 @@ - + diff --git a/client/src/router/index.js b/client/src/router/index.js index a7bdd9f6..0c8a651c 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -6,7 +6,7 @@ import Project from "@/views/Project.vue"; import Task from "@/views/Task.vue"; import Home from "@/views/Home.vue"; import userAPI from "@/api/user"; -import TaskDetail from "@/components/task/TaskDetail.vue"; +// import TaskDetail from "@/components/task/TaskDetail.vue"; Vue.use(VueRouter); @@ -50,7 +50,7 @@ const routes = [ { path: "task/:taskId", name: "TodayTaskDetail", - component: TaskDetail, + component: Task, }, ], }, @@ -63,7 +63,7 @@ const routes = [ { path: "task/:taskId", name: "ProjectTaskDetail", - component: TaskDetail, + component: Task, }, ], }, diff --git a/client/src/views/Task.vue b/client/src/views/Task.vue index bd219277..9128b372 100644 --- a/client/src/views/Task.vue +++ b/client/src/views/Task.vue @@ -1,11 +1,50 @@ From 860e6f6b0cfbf7f1da24d471623c7a422969727a Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Fri, 4 Dec 2020 03:26:14 +0900 Subject: [PATCH 154/281] =?UTF-8?q?[FE]=20REFACTOR:=20task=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=EC=A0=90=EC=97=90=20async=20await=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/views/Task.vue | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/client/src/views/Task.vue b/client/src/views/Task.vue index 9128b372..96a17d3a 100644 --- a/client/src/views/Task.vue +++ b/client/src/views/Task.vue @@ -3,7 +3,7 @@ @@ -29,22 +29,13 @@ export default { }, computed: { ...mapGetters(["currentTask", "namedProjectInfos"]), - getProjectTitle() { - return this.namedProjectInfos.find((project) => project.id === this.currentTask.projectId) - .title; - }, }, - created() { - // console.log(this); - this.fetchCurrentTask(this.$route.params.taskId); + async created() { + await this.fetchCurrentTask(this.$route.params.taskId); + this.projectTitle = this.namedProjectInfos.find( + (project) => project.id === this.currentTask.projectId + ).title; }, - // mounted() { - // this.projectTitle = this.getProjectTitle(); - // }, - // beforeRouteUpdate(to, from, next) { - // this.fetchCurrentTask(to.params.taskId); - // next(); - // }, }; From 458854e2466f529f07a5248aa209c4e067f4ebc4 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Fri, 4 Dec 2020 05:16:07 +0900 Subject: [PATCH 155/281] =?UTF-8?q?[FE]=20FEAT:=20alert=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B0=8F=20store=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v-alert을 API 로직과 연동하여 에러가 나면 alert을 띄워주기 위해 alert 컴포넌트와 alert의 state 값을 store로 구현 --- client/src/components/common/Alert.vue | 37 ++++++++++++++++++++++++++ client/src/store/alert.js | 31 +++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 client/src/components/common/Alert.vue create mode 100644 client/src/store/alert.js diff --git a/client/src/components/common/Alert.vue b/client/src/components/common/Alert.vue new file mode 100644 index 00000000..684d6b18 --- /dev/null +++ b/client/src/components/common/Alert.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/client/src/store/alert.js b/client/src/store/alert.js new file mode 100644 index 00000000..cc3b48e0 --- /dev/null +++ b/client/src/store/alert.js @@ -0,0 +1,31 @@ +const state = { + alert: { + message: "", + type: "", + }, +}; + +const mutations = { + SET_ERROR_ALERT(state, { data, status }) { + if (status === 401) { + state.alert = { message: "세션이 만료되었습니다", type: "error" }; + return; + } else { + state.alert = { message: data.message, type: "error" }; + } + }, + CLEAR_ALERT(state) { + state.alert = { message: "", type: "" }; + }, +}; + +const getters = {}; + +const actions = {}; + +export default { + state, + mutations, + getters, + actions, +}; From af075499f1faafb3894602eca1fec1038197d4b0 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Fri, 4 Dec 2020 05:18:19 +0900 Subject: [PATCH 156/281] =?UTF-8?q?[FE]=20REFACTOR:=20URLSearchParasm=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - accessToken을 location.search를 split 하던것을 URLSearchParams 인스턴스로 변경 --- client/src/App.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/App.vue b/client/src/App.vue index b0d174f1..f8612e26 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -11,10 +11,9 @@ export default { data: () => ({ // }), - created() { + beforeCreate() { // token 저장 - // eslint-disable-next-line no-unused-vars - const [_, accessToken] = location.search.split("token="); + const accessToken = new URLSearchParams(location.search).get("token"); if (accessToken) { localStorage.setItem("token", accessToken); router.replace("/"); @@ -30,5 +29,6 @@ export default { return; } }, + created() {}, }; From 04b6d75fc97fb2fc0b7cd5440dcef955732118c0 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Fri, 4 Dec 2020 05:19:44 +0900 Subject: [PATCH 157/281] =?UTF-8?q?[FE]=20FEAT:=20=EA=B0=81=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20alert=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/menu/LeftMenu.vue | 17 +++++++++++------ .../src/components/project/ProjectContainer.vue | 9 +++++++-- client/src/views/Login.vue | 4 ++++ client/src/views/Project.vue | 9 +++++++-- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/client/src/components/menu/LeftMenu.vue b/client/src/components/menu/LeftMenu.vue index fc95afb4..ec712274 100644 --- a/client/src/components/menu/LeftMenu.vue +++ b/client/src/components/menu/LeftMenu.vue @@ -8,6 +8,7 @@ +
@@ -16,6 +17,7 @@ import FavoriteProjectList from "./FavoriteProjectList"; import ProjectListContainer from "./ProjectListContainer"; import LabelList from "./LabelList"; import FilterList from "./FilterList"; +import Alert from "@/components/common/Alert"; import { mapGetters, mapActions } from "vuex"; export default { @@ -24,6 +26,7 @@ export default { ProjectListContainer, LabelList, FilterList, + Alert, }, computed: mapGetters([ "namedProjectInfos", @@ -44,12 +47,14 @@ export default { "fetchAllTasks", ]), }, - created() { - this.fetchAllTasks(); - this.fetchProjectInfos(); - this.fetchTodayProject(); - this.fetchLabels(); - this.fetchPriorities(); + mounted() { + this.$nextTick(() => { + this.fetchAllTasks(); + this.fetchProjectInfos(); + this.fetchTodayProject(); + this.fetchLabels(); + this.fetchPriorities(); + }); }, }; diff --git a/client/src/components/project/ProjectContainer.vue b/client/src/components/project/ProjectContainer.vue index 68d8a51a..1f2220f2 100644 --- a/client/src/components/project/ProjectContainer.vue +++ b/client/src/components/project/ProjectContainer.vue @@ -32,7 +32,12 @@
- + @@ -76,7 +81,7 @@ export default { }, hideTaskModal() { router.push(`/project/${this.projectId}`); - }, + }, }, components: { AddTask, TaskItem, UpdatableTitle }, }; diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index 4443b10f..f2227ef2 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -7,12 +7,16 @@
+ From b3cb17185bba9507a4183f2c7fa5ae578a3c5aeb Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Fri, 4 Dec 2020 05:20:06 +0900 Subject: [PATCH 158/281] =?UTF-8?q?[FE]=20REFACTOR:=20store=EC=97=90=20err?= =?UTF-8?q?or=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EB=B6=80=EB=B6=84=20commit(?= =?UTF-8?q?"SET=5FERROR=5FALERT")=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/auth.js | 2 +- client/src/store/index.js | 3 ++- client/src/store/label.js | 2 +- client/src/store/priority.js | 2 +- client/src/store/project.js | 25 +++++++++++++------------ client/src/store/task.js | 5 +++-- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/client/src/store/auth.js b/client/src/store/auth.js index e9b9c419..6e7c0129 100644 --- a/client/src/store/auth.js +++ b/client/src/store/auth.js @@ -38,7 +38,7 @@ const actions = { return; } } catch (err) { - alert("로그인에 실패했습니다."); + commit("SET_ERROR_ALERT", err.message); router.replace("/login").catch(() => {}); return; } diff --git a/client/src/store/index.js b/client/src/store/index.js index 902f905a..d4dfb398 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -5,9 +5,10 @@ import project from "./project"; import label from "./label"; import priority from "./priority"; import task from "./task"; +import alert from "./alert"; Vue.use(Vuex); export default new Vuex.Store({ - modules: { auth, project, label, priority, task }, + modules: { auth, project, label, priority, task, alert }, }); diff --git a/client/src/store/label.js b/client/src/store/label.js index 39a63536..c76e2b06 100644 --- a/client/src/store/label.js +++ b/client/src/store/label.js @@ -20,7 +20,7 @@ const actions = { commit("SET_LABELS", labels); } catch (err) { - alert("라벨 전체 정보 조회 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, }; diff --git a/client/src/store/priority.js b/client/src/store/priority.js index f00c6513..aca8a310 100644 --- a/client/src/store/priority.js +++ b/client/src/store/priority.js @@ -33,7 +33,7 @@ const actions = { ]; commit("SET_PRIORITIES", priorities); } catch (err) { - alert("우선순위 전체 정보 조회 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, }; diff --git a/client/src/store/project.js b/client/src/store/project.js index 1f0cd8d0..f3cb2aa3 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -30,7 +30,7 @@ const actions = { commit("SET_CURRENT_PROJECT", project); } catch (err) { - alert("프로젝트 조회 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, @@ -40,11 +40,11 @@ const actions = { commit("SET_TODAY_PROJECT", todayProject); } catch (err) { - alert("오늘의 프로젝트 조회 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, - async updateProjectTitle({ dispatch }, { projectId, title }) { + async updateProjectTitle({ dispatch, commit }, { projectId, title }) { try { const { data } = await projectAPI.updateProject(projectId, { title }); @@ -54,11 +54,11 @@ const actions = { await dispatch("fetchCurrentProject", projectId); } catch (err) { - alert("프로젝트 수정 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, - async updateSectionTitle({ dispatch }, { projectId, sectionId, title }) { + async updateSectionTitle({ dispatch, commit }, { projectId, sectionId, title }) { try { const { data } = await projectAPI.updateSection(projectId, sectionId, { title }); @@ -68,11 +68,11 @@ const actions = { await dispatch("fetchCurrentProject", projectId); } catch (err) { - alert("섹션 수정 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, - async updateTaskToDone({ dispatch }, { projectId, taskId }) { + async updateTaskToDone({ dispatch, commit }, { projectId, taskId }) { try { const { data } = await taskAPI.updateTask(taskId, { isDone: true }); @@ -82,11 +82,11 @@ const actions = { await dispatch("fetchCurrentProject", projectId); } catch (err) { - alert("프로젝트 수정 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, - async addTask({ dispatch }, task) { + async addTask({ dispatch, commit }, task) { try { const { data } = await taskAPI.createTask(task); @@ -95,8 +95,8 @@ const actions = { } await dispatch("fetchCurrentProject", task.projectId); - } catch { - alert("프로젝트 추가 요청 실패"); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); } }, @@ -106,7 +106,8 @@ const actions = { commit("SET_PROJECT_INFOS", projectInfos); } catch (err) { - alert("프로젝트 전체 정보 조회 요청 실패"); + commit("SET_ERROR_ALERT", err.response); + // alert("프로젝트 전체 정보 조회 요청 실패"); } }, diff --git a/client/src/store/task.js b/client/src/store/task.js index 19c7bdba..f3507180 100644 --- a/client/src/store/task.js +++ b/client/src/store/task.js @@ -24,7 +24,8 @@ const actions = { const { data: tasks } = await taskAPI.getAllTasks(); commit("SET_TASKS", tasks); } catch (err) { - alert("작업 전체 조회 요청 실패"); + commit("SET_ERROR_ALERT", err.response); + // alert("작업 전체 조회 요청 실패"); } }, startDragTask({ commit }, { task }) { @@ -35,7 +36,7 @@ const actions = { const { data: task } = await taskAPI.getTaskById(taskId); commit("SET_CURRENT_TASK", task); } catch (err) { - alert("작업 상세 조회 요청 실패"); + commit("SET_ERROR_ALERT", err.response); } }, }; From 50e2a3ef40fc48649947cd70583595d9f3a576af Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Fri, 4 Dec 2020 11:06:59 +0900 Subject: [PATCH 159/281] =?UTF-8?q?[FE]=20REFACTOR:=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=20=EB=A7=8C=EB=A3=8C=20alert=EC=8B=9C=20login=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/alert.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/store/alert.js b/client/src/store/alert.js index cc3b48e0..3301a7eb 100644 --- a/client/src/store/alert.js +++ b/client/src/store/alert.js @@ -1,3 +1,5 @@ +import router from "@/router"; + const state = { alert: { message: "", @@ -9,6 +11,7 @@ const mutations = { SET_ERROR_ALERT(state, { data, status }) { if (status === 401) { state.alert = { message: "세션이 만료되었습니다", type: "error" }; + router.replace("/login").catch(() => {}); return; } else { state.alert = { message: data.message, type: "error" }; From 2299140d5ffd44d96e7efdc28b52ef6a2b3f7521 Mon Sep 17 00:00:00 2001 From: shkilo Date: Fri, 4 Dec 2020 19:07:12 +0900 Subject: [PATCH 160/281] =?UTF-8?q?[FE]=20FIX:=20AddTask=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20state=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/project/AddTask.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/project/AddTask.vue b/client/src/components/project/AddTask.vue index 212b3749..4a71c9a0 100644 --- a/client/src/components/project/AddTask.vue +++ b/client/src/components/project/AddTask.vue @@ -59,7 +59,7 @@ export default { projectId: this.projectId, sectionId: this.sectionId, title: "", - dueDate: new Date(), + dueDate: getTodayString(), }; this.show = !this.show; }, From 3fffa0dafed0500f37f97afacd3662395e0df069 Mon Sep 17 00:00:00 2001 From: shkilo Date: Fri, 4 Dec 2020 19:08:17 +0900 Subject: [PATCH 161/281] =?UTF-8?q?[FE]=20FIX:=20updatabelTitle=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20watch=20=ED=9B=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20+=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8?= =?UTF-8?q?=20=EB=9D=BC=EC=9A=B0=ED=8C=85=EC=9C=BC=EB=A1=9C=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C=EC=9A=B4=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=80=20=EB=9E=9C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EB=90=A0=EB=95=8C=20updatable=20title=EC=9D=98=20s?= =?UTF-8?q?tate=EA=B0=80=20=EA=B0=B1=EC=8B=A0=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/common/UpdatableTitle.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/components/common/UpdatableTitle.vue b/client/src/components/common/UpdatableTitle.vue index d4880030..94a9a8b4 100644 --- a/client/src/components/common/UpdatableTitle.vue +++ b/client/src/components/common/UpdatableTitle.vue @@ -60,6 +60,12 @@ export default { this.toggle(); }, }, + watch: { + originalTitle: function () { + this.newTitle = this.originalTitle; + this.showForm = false; + }, + }, }; From 28c69cb96052c44700a4efd54196cef3591eb64c Mon Sep 17 00:00:00 2001 From: shkilo Date: Sat, 5 Dec 2020 21:15:59 +0900 Subject: [PATCH 162/281] =?UTF-8?q?[FE]=20REFACTOR:=20ProjectContainer=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20SectionContainer=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC,=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=EC=95=A4=20=EB=93=9C=EB=9E=8D=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + SectionContiner 분리 + ProjectContiner 에서 draggingTask 상태 관리 및 sectionContainer에 props로 전달 --- .../components/project/ProjectContainer.vue | 41 +++++------- .../components/project/SectionContainer.vue | 66 +++++++++++++++++++ client/src/components/project/TaskItem.vue | 15 +++-- client/src/router/index.js | 2 +- 4 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 client/src/components/project/SectionContainer.vue diff --git a/client/src/components/project/ProjectContainer.vue b/client/src/components/project/ProjectContainer.vue index 14f8c065..729fac9e 100644 --- a/client/src/components/project/ProjectContainer.vue +++ b/client/src/components/project/ProjectContainer.vue @@ -25,30 +25,20 @@
- - - - - - -
- - - -
- -
-
- - -
+
@@ -71,9 +68,7 @@ export default { .project-header { min-width: 450px; } -.task-container { - min-width: 450px; -} + .project-container { width: 100%; max-width: 600px; diff --git a/client/src/components/project/SectionContainer.vue b/client/src/components/project/SectionContainer.vue new file mode 100644 index 00000000..55e04fa3 --- /dev/null +++ b/client/src/components/project/SectionContainer.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/client/src/components/project/TaskItem.vue b/client/src/components/project/TaskItem.vue index ef49929f..7c67adfe 100644 --- a/client/src/components/project/TaskItem.vue +++ b/client/src/components/project/TaskItem.vue @@ -57,19 +57,20 @@ export default { }, handleDragStart() { this.dragging = true; - this.startDragTask({ - task: { - ...this.task, - position: this.position, - }, - }); + this.$emit("taskDragStart", this.task); + // this.startDragTask({ + // task: { + // ...this.task, + // position: this.position, + // }, + // }); }, handleDragEnd() { this.dragging = false; }, handleDragOver() { this.draggedOver = true; - + this.$emit("taskDragOver", this.position); // const offset = this.middleY - e.clientY; // if (offset < 0) { // this.changeTaskPosition({ diff --git a/client/src/router/index.js b/client/src/router/index.js index 3ca2bceb..3a4028c3 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -4,8 +4,8 @@ import Login from "@/views/Login.vue"; import Today from "@/views/Today.vue"; import Project from "@/views/Project.vue"; import Home from "@/views/Home.vue"; +import Task from "@/views/Task"; import userAPI from "@/api/user"; -// import TaskDetail from "@/components/task/TaskDetail.vue"; Vue.use(VueRouter); From c10cf7d3d7cd75f0e8e96b8b97be65691f54ea92 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Sun, 6 Dec 2020 20:52:48 +0900 Subject: [PATCH 163/281] =?UTF-8?q?[BE]=20FIX:=20=EC=A0=84=EC=B2=B4=20task?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20key=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20label=20todo=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/task.js | 2 +- server/src/routes/label.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index 2fa8135c..b8d5ab3b 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -12,7 +12,7 @@ const getTaskById = asyncTryCatch(async (req, res) => { const getAllTasks = asyncTryCatch(async (req, res) => { const tasks = await taskService.retrieveAll(req.user.id); - responseHandler(res, 200, tasks); + responseHandler(res, 200, { tasks }); }); const createTask = asyncTryCatch(async (req, res) => { diff --git a/server/src/routes/label.js b/server/src/routes/label.js index 5147c306..ace00852 100644 --- a/server/src/routes/label.js +++ b/server/src/routes/label.js @@ -1,6 +1,10 @@ const router = require('express').Router(); const labelController = require('@controllers/label'); +// TODO: 리팩토링 해야한다 +// controller에서 validation check는 받은 정보가 맞는 타입인지만 체크 +// middle ware는 controller로 가는 과정에서 체크되거나 추가되는 정보 +// 아래에 own label이나 이런 것들은 서비스에서 책임을 져야한다. router.get('/', labelController.getAllLabels); router.post('/', labelController.isValidRequestDatas, labelController.createLabel); router.put( From 6b111e75b38d9354c6ef67fc31bd0aa0f473c52b Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Sun, 6 Dec 2020 21:05:30 +0900 Subject: [PATCH 164/281] =?UTF-8?q?[FE]=20REFACTOR:=20API=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20store=20=EB=B3=80=EA=B2=BD=20&=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B2=B0=EA=B3=BC=20router=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/Search.vue | 10 ++++++---- client/src/store/task.js | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/client/src/components/task/Search.vue b/client/src/components/task/Search.vue index 0bd48a63..0ed68c99 100644 --- a/client/src/components/task/Search.vue +++ b/client/src/components/task/Search.vue @@ -26,7 +26,6 @@ - + From 2be740fc5b47d87c35b48e0a8c9865de3c5dd300 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 7 Dec 2020 01:15:59 +0900 Subject: [PATCH 166/281] =?UTF-8?q?[FE]=20REFACTOR:=20keep-alive=EB=A5=BC?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=ED=95=98=EC=97=AC=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - projectList state 생성 - fetchCurrentProject 수정 - projectList[projectId]와 같은 식으로 component의 props 다르게 적용 - 각 컴포넌트에서 상태값을 유지하도록 keep-alive로 router-view 감싸기 --- client/src/store/project.js | 9 ++++++++- client/src/views/Home.vue | 4 +++- client/src/views/Project.vue | 17 ++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/client/src/store/project.js b/client/src/store/project.js index f3cb2aa3..35601101 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -10,6 +10,7 @@ const state = { sections: [], }, projectInfos: [], + projectList: {}, todayProject: { id: "", count: 0, @@ -21,6 +22,7 @@ const getters = { todayProject: (state) => state.todayProject, namedProjectInfos: (state) => state.projectInfos.filter((project) => project.title !== "관리함"), managedProject: (state) => state.projectInfos.find((project) => project.title === "관리함"), + projectList: (state) => state.projectList, }; const actions = { @@ -141,7 +143,12 @@ const actions = { const mutations = { //TODO: function vs arrow-function style-guide 보고 통일하기 - SET_CURRENT_PROJECT: (state, currentProject) => (state.currentProject = currentProject), + SET_CURRENT_PROJECT: (state, currentProject) => { + const newlyAddedProject = {}; + newlyAddedProject[currentProject.id] = currentProject; + state.projectList = { ...state.projectList, ...newlyAddedProject }; + state.currentProject = currentProject; + }, SET_PROJECT_INFOS: (state, projectInfos) => (state.projectInfos = projectInfos), SET_TODAY_PROJECT: (state, todayProject) => (state.todayProject = todayProject), // newTodo: (state, todo) => state.todos.unshift(todo), diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index becd9a44..befc26fb 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -3,7 +3,9 @@
- + + +
diff --git a/client/src/views/Project.vue b/client/src/views/Project.vue index 5bc43030..729c4b2b 100644 --- a/client/src/views/Project.vue +++ b/client/src/views/Project.vue @@ -1,12 +1,12 @@ From 44eec056558bdd3c25dfb3002149136f0e171b35 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 7 Dec 2020 02:19:36 +0900 Subject: [PATCH 167/281] =?UTF-8?q?[FE]=20REFACTOR:=20isAuth=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=B4=20home=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - store.auth에 isAuth값을 설정 - v-if를 통해 home을 렌더링할 것인지 여부를 결정 - 불필요한 렌더링과 API 호출을 방지 --- client/src/App.vue | 3 +-- client/src/components/common/Header.vue | 9 +++++++-- client/src/components/menu/LeftMenu.vue | 14 ++++++-------- client/src/store/auth.js | 2 ++ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/client/src/App.vue b/client/src/App.vue index f8612e26..3b093f9c 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -11,7 +11,7 @@ export default { data: () => ({ // }), - beforeCreate() { + created() { // token 저장 const accessToken = new URLSearchParams(location.search).get("token"); if (accessToken) { @@ -29,6 +29,5 @@ export default { return; } }, - created() {}, }; diff --git a/client/src/components/common/Header.vue b/client/src/components/common/Header.vue index 87913961..2570ba90 100644 --- a/client/src/components/common/Header.vue +++ b/client/src/components/common/Header.vue @@ -1,7 +1,7 @@ From 17e97de122380c611863f12718bdaa78394e31e7 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 16:27:02 +0900 Subject: [PATCH 182/281] =?UTF-8?q?[FE]=20FEAT:=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=BD=94=EB=A9=98=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/task.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/api/task.js b/client/src/api/task.js index af554823..338c387a 100644 --- a/client/src/api/task.js +++ b/client/src/api/task.js @@ -13,6 +13,9 @@ const taskAPI = { updateTask(taskId, data) { return myAxios.patch(`/task/${taskId}`, data); }, + getAllComments(taskId) { + return myAxios.get(`/task/${taskId}/comment`); + }, }; export default taskAPI; From 583c9e602e06471af46df90277f8513aa3f8e041 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Mon, 7 Dec 2020 16:48:55 +0900 Subject: [PATCH 183/281] =?UTF-8?q?[FE]=20FEAT:=20hover=20=EC=8B=9C=20+=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=82=98=ED=83=80=EB=82=98=EA=B2=8C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/menu/FavoriteProjectList.vue | 4 ++-- client/src/components/menu/FilterList.vue | 13 ++++++++++--- client/src/components/menu/LabelList.vue | 13 ++++++++++--- client/src/components/menu/ProjectListContainer.vue | 13 ++++++++++--- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index 608dee19..0f6fe9e7 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -5,7 +5,7 @@ mdi-inbox - + 관리함 {{ managedProject.taskCount }} @@ -15,7 +15,7 @@ mdi-calendar-today - + 오늘 {{ taskCount }} diff --git a/client/src/components/menu/FilterList.vue b/client/src/components/menu/FilterList.vue index d242e89e..661965b4 100644 --- a/client/src/components/menu/FilterList.vue +++ b/client/src/components/menu/FilterList.vue @@ -1,9 +1,16 @@ + + From cfa7074199859240ad9e91a28daea6ba0c911aa9 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 23:09:39 +0900 Subject: [PATCH 197/281] =?UTF-8?q?[FE]=20STYLE:=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=BC=20=EB=95=8C,=20=ED=83=AD=EC=9D=B4?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=EC=9D=98=20=EA=B0=80=EB=A1=9C=EC=97=90=20?= =?UTF-8?q?=EC=B1=84=EC=9B=8C=EC=A7=80=EB=8F=84=EB=A1=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/TaskDetailTabs.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/components/task/TaskDetailTabs.vue b/client/src/components/task/TaskDetailTabs.vue index 2aa95cf2..e8968106 100644 --- a/client/src/components/task/TaskDetailTabs.vue +++ b/client/src/components/task/TaskDetailTabs.vue @@ -51,4 +51,8 @@ export default { }; - + From 6be508cc75fc710e8ef57d4981d234d675308207 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 23:10:17 +0900 Subject: [PATCH 198/281] =?UTF-8?q?[FE]=20FEAT:=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20container=EC=97=90=EC=84=9C=20comment=20co?= =?UTF-8?q?unt=20=EB=84=98=EA=B8=B0=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/TaskDetailContainer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/task/TaskDetailContainer.vue b/client/src/components/task/TaskDetailContainer.vue index 3fd9eb66..652d4563 100644 --- a/client/src/components/task/TaskDetailContainer.vue +++ b/client/src/components/task/TaskDetailContainer.vue @@ -54,7 +54,7 @@ export default { computed: {}, created() { this.tabList.childTask.count = this.task.tasks.length; - this.tabList.comment.count = 1; + this.tabList.comment.count = this.comments.length; this.tabList.bookmark.count = 3; }, mounted() {}, From 25712c4a27ef437fe444473f58b63d6697c4d5b2 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 23:10:54 +0900 Subject: [PATCH 199/281] =?UTF-8?q?[FE]=20REFACTOR:=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=EB=90=9C=20=EC=9E=91=EC=97=85=EC=9D=80=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/ChildTaskList.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/components/task/ChildTaskList.vue b/client/src/components/task/ChildTaskList.vue index ef77e323..3a069b82 100644 --- a/client/src/components/task/ChildTaskList.vue +++ b/client/src/components/task/ChildTaskList.vue @@ -1,7 +1,6 @@ @@ -17,6 +16,11 @@ export default { sectionId: String, parentId: String, }, + computed: { + opendTasks() { + return this.tasks.filter((task) => !task.isDone); + }, + }, components: { TaskItem, AddTask }, }; From 36212388ecad049ab69e376f2314adff81d1e4d7 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 23:12:20 +0900 Subject: [PATCH 200/281] =?UTF-8?q?[FE]=20REFACTOR:=20CommentItem=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/CommentList.vue | 39 +++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/client/src/components/task/CommentList.vue b/client/src/components/task/CommentList.vue index 466d80fd..3aebee28 100644 --- a/client/src/components/task/CommentList.vue +++ b/client/src/components/task/CommentList.vue @@ -1,27 +1,23 @@ - + From e0045c71888fb3cccc822014a793743d2b24b8ff Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 23:12:47 +0900 Subject: [PATCH 201/281] =?UTF-8?q?[FE]=20FEAT:=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=EC=9D=98=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20item=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/CommentItem.vue | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 client/src/components/task/CommentItem.vue diff --git a/client/src/components/task/CommentItem.vue b/client/src/components/task/CommentItem.vue new file mode 100644 index 00000000..ca1908f3 --- /dev/null +++ b/client/src/components/task/CommentItem.vue @@ -0,0 +1,54 @@ + + + From ecc4ea9eeda88d4e0588fbbb1f8e676e773a0160 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 23:18:28 +0900 Subject: [PATCH 202/281] =?UTF-8?q?[FE]=20FEAT:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/comment.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/api/comment.js b/client/src/api/comment.js index 222a06be..1873ab09 100644 --- a/client/src/api/comment.js +++ b/client/src/api/comment.js @@ -7,6 +7,9 @@ const labelAPI = { createComment(data) { return myAxios.post(`/task/${data.taskId}/comment`, data); }, + deleteComment(comment) { + return myAxios.delete(`/task/${comment.taskId}/comment/${comment.id}`, comment); + }, }; export default labelAPI; From a0dce68a5064e22089f6403aef48d13eeb129a7e Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Mon, 7 Dec 2020 23:39:34 +0900 Subject: [PATCH 203/281] =?UTF-8?q?[FE]=20FEAT:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/task/CommentItem.vue | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/components/task/CommentItem.vue b/client/src/components/task/CommentItem.vue index ca1908f3..d80df68a 100644 --- a/client/src/components/task/CommentItem.vue +++ b/client/src/components/task/CommentItem.vue @@ -14,7 +14,7 @@ mdi-pencil 수정 - + mdi-delete 삭제 @@ -22,6 +22,7 @@
+ diff --git a/client/src/components/task/CommentList.vue b/client/src/components/task/CommentList.vue index 3aebee28..15eee2e5 100644 --- a/client/src/components/task/CommentList.vue +++ b/client/src/components/task/CommentList.vue @@ -3,50 +3,20 @@ -
-
- - 댓글 추가 -
- -
+
{{ this.comments }}
- + From 2221d2f5d80c38962dfa18819560ce817b8ff004 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 00:56:48 +0900 Subject: [PATCH 208/281] =?UTF-8?q?[FE]=20FEAT:=20comment=20update=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/comment.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/api/comment.js b/client/src/api/comment.js index 1873ab09..c577fc0f 100644 --- a/client/src/api/comment.js +++ b/client/src/api/comment.js @@ -7,6 +7,9 @@ const labelAPI = { createComment(data) { return myAxios.post(`/task/${data.taskId}/comment`, data); }, + updateComment(comment) { + return myAxios.put(`/task/${comment.taskId}/comment/${comment.id}`, comment); + }, deleteComment(comment) { return myAxios.delete(`/task/${comment.taskId}/comment/${comment.id}`, comment); }, From b24ea9e6ef8f6ccd3593922e8959a16cab06d598 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 00:58:59 +0900 Subject: [PATCH 209/281] =?UTF-8?q?[FE]=20comment=20store=EC=97=90=20comme?= =?UTF-8?q?nt=20update=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/comment.js | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/client/src/store/comment.js b/client/src/store/comment.js index 032e891a..a42acca7 100644 --- a/client/src/store/comment.js +++ b/client/src/store/comment.js @@ -4,13 +4,12 @@ const SUCCESS_MESSAGE = "ok"; const state = { comments: [], + commentCounts: 0, }; const getters = { comments: (state) => state.comments, - // commentsCount: (state) => { - // return state.tasks.reduce((acc, task) => acc + task.tasks.length, state.tasks.length); - // }, + commentCounts: (state) => state.comments.length, }; const actions = { @@ -19,6 +18,7 @@ const actions = { const { data: comments } = await commentAPI.getAllComments(taskId); comments.sort((comment1, comment2) => (comment1.updatedAt > comment2.updatedAt ? 1 : -1)); commit("SET_COMMENTS", comments); + commit("SET_COMMENT_COUNTS", comments.length); } catch (err) { commit("SET_ERROR_ALERT", err.response); // alert("작업 전체 조회 요청 실패"); @@ -36,32 +36,38 @@ const actions = { commit("SET_ERROR_ALERT", err.response); } }, - async deleteComment({ commit }, comment) { + + async updateComment({ commit, dispatch }, comment) { + try { + const { data } = await commentAPI.updateComment(comment); + if (data.message !== SUCCESS_MESSAGE) { + throw Error; + } + await dispatch("fetchComments", comment.taskId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async deleteComment({ commit, dispatch }, comment) { try { const { data } = await commentAPI.deleteComment(comment); if (data.message !== SUCCESS_MESSAGE) { throw Error; } - // await dispatch("fetchComments", comment.taskId); - commit("DELETE_COMMENT", comment.id); + await dispatch("fetchComments", comment.taskId); + // commit("DELETE_COMMENT", comment.id); + // commit("DECREASE_COMMENT_COUNTS"); } catch (err) { commit("SET_ERROR_ALERT", err.response); } }, - // async fetchUpdateComment({ commit }, comment) { - // try { - // // const { data: comment } = await commentAPI.getTaskById(taskId, commentId); - // // commit("SET_CURRENT_TASK", task); - // } catch (err) { - // commit("SET_ERROR_ALERT", err.response); - // } - // }, }; const mutations = { SET_COMMENTS: (state, comments) => (state.comments = comments), - + SET_COMMENT_COUNTS: (state, counts) => (state.commentCounts = counts), + DECREASE_COMMENT_COUNTS: (state) => state.commentCounts--, // UPDATE_COMMENT: (state, comment) => (state.comments.find(comment => comment.id) = comment); DELETE_COMMENT: (state, commentId) => { const index = state.comments.indexOf( From eadbe4c9717019441c5b5b406c9dc0c6e39044f9 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 01:06:51 +0900 Subject: [PATCH 210/281] =?UTF-8?q?[BE]=20REFACTOR:=20api=20test=EC=97=90?= =?UTF-8?q?=20color=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/project.api.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/test/project.api.test.js b/server/test/project.api.test.js index 6031cd19..570ec9b4 100644 --- a/server/test/project.api.test.js +++ b/server/test/project.api.test.js @@ -72,6 +72,7 @@ describe('create project', () => { // given const requestBody = { title: '새 프로젝트', + color: '#FFA7A7', isList: true, }; const expectedUser = seeder.users[0]; @@ -95,6 +96,7 @@ describe('update project', () => { const expectedProjectId = seeder.projects[0].id; const requestBody = { title: 'PUT으로 변경된 프로젝트', + color: '#FFA7A7', isList: true, isFavorite: true, }; From 8635477444c1941df36d3e07296fb449730cbb44 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 01:18:07 +0900 Subject: [PATCH 211/281] =?UTF-8?q?[BE]=20REFACTOR:=20project=20api=20retu?= =?UTF-8?q?rn=20json=EC=97=90=20projectId=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/project.js | 4 ++-- server/src/services/project.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/controllers/project.js b/server/src/controllers/project.js index 6f8116ec..f75bea6b 100644 --- a/server/src/controllers/project.js +++ b/server/src/controllers/project.js @@ -22,9 +22,9 @@ const getProjectById = asyncTryCatch(async (req, res) => { const createProject = asyncTryCatch(async (req, res) => { const { id: creatorId } = req.user; - await projectService.create({ creatorId, ...req.body }); + const projectId = await projectService.create({ creatorId, ...req.body }); - responseHandler(res, 201, { message: 'ok' }); + responseHandler(res, 201, { message: 'ok', projectId }); }); const updateProject = asyncTryCatch(async (req, res) => { diff --git a/server/src/services/project.js b/server/src/services/project.js index 35fc8832..c9dbf4b6 100644 --- a/server/src/services/project.js +++ b/server/src/services/project.js @@ -93,7 +93,7 @@ const create = async data => { return section.projectId; }); - return !!result; + return result; }; const findOrCreate = async data => { From 6034a9c18f991f8d9b55efe2157f50464fc11908 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 01:19:21 +0900 Subject: [PATCH 212/281] =?UTF-8?q?[FE]=20FIX:=20Alert=20component=20z-ind?= =?UTF-8?q?ex=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/common/Alert.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/components/common/Alert.vue b/client/src/components/common/Alert.vue index 94539434..3f8ac4cf 100644 --- a/client/src/components/common/Alert.vue +++ b/client/src/components/common/Alert.vue @@ -22,7 +22,9 @@ export default { ...mapMutations(["CLEAR_ALERT"]), }, beforeUpdate() { - setTimeout(() => this.CLEAR_ALERT(), 3000); + if (this.alert.message) { + setTimeout(() => this.CLEAR_ALERT(), 3000); + } }, }; @@ -33,5 +35,6 @@ export default { top: 0; left: 50%; transform: translate(-50%, 0); + z-index: 100; } From f1a0b09bfe6ba7c6b75206ec6e17e548f2c86169 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 01:20:09 +0900 Subject: [PATCH 213/281] =?UTF-8?q?[FE]=20FIX:=20alert=20SET=5FSUCCESS=5FA?= =?UTF-8?q?LERT=20mutation=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/alert.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/store/alert.js b/client/src/store/alert.js index 3301a7eb..bea319fe 100644 --- a/client/src/store/alert.js +++ b/client/src/store/alert.js @@ -20,6 +20,9 @@ const mutations = { CLEAR_ALERT(state) { state.alert = { message: "", type: "" }; }, + SET_SUCCESS_ALERT(state, message) { + state.alert = { message, type: "success" }; + }, }; const getters = {}; From bbb4a80f4ab3a08fce26a0a7aba4539af457e45b Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 01:20:55 +0900 Subject: [PATCH 214/281] =?UTF-8?q?[FE]=20FEAT:=20project=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api , store에 관련 함수 추가 - 생성 시 완료 메시지 후 새로운 프로젝트 화면으로 라우팅 --- client/src/api/project.js | 3 +++ .../components/project/AddProjectModal.vue | 19 +++++++++++++++---- client/src/store/project.js | 11 +++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/client/src/api/project.js b/client/src/api/project.js index a53ef14e..5664fba5 100644 --- a/client/src/api/project.js +++ b/client/src/api/project.js @@ -10,6 +10,9 @@ const projectAPI = { getTodayProject() { return myAxios.get("/project/today"); }, + createProject(data) { + return myAxios.post("/project", data); + }, updateProject(projectId, data) { return myAxios.patch(`/project/${projectId}`, data); }, diff --git a/client/src/components/project/AddProjectModal.vue b/client/src/components/project/AddProjectModal.vue index e8019121..d8478faa 100644 --- a/client/src/components/project/AddProjectModal.vue +++ b/client/src/components/project/AddProjectModal.vue @@ -5,7 +5,7 @@ persistent max-width="600" class="add-project-dialog" - @click:outside="sendEvent" + @click:outside="sendCloseModalEvent" > 프로젝트 추가 @@ -60,8 +60,8 @@ - 취소 - 추가 + 취소 + 추가
@@ -70,6 +70,7 @@ diff --git a/client/src/components/menu/FilterList.vue b/client/src/components/menu/FilterList.vue index 661965b4..cee5b7f2 100644 --- a/client/src/components/menu/FilterList.vue +++ b/client/src/components/menu/FilterList.vue @@ -17,7 +17,7 @@ mdi-flag - + {{ priority.title }} diff --git a/client/src/components/menu/LabelList.vue b/client/src/components/menu/LabelList.vue index e5637d0b..34f53857 100644 --- a/client/src/components/menu/LabelList.vue +++ b/client/src/components/menu/LabelList.vue @@ -17,7 +17,7 @@ mdi-label - + {{ label.title }} diff --git a/client/src/components/menu/LeftMenu.vue b/client/src/components/menu/LeftMenu.vue index 44ad17b4..865fe409 100644 --- a/client/src/components/menu/LeftMenu.vue +++ b/client/src/components/menu/LeftMenu.vue @@ -4,6 +4,7 @@ v-if="managedProject" :managed-project="managedProject" :task-count="taskCount" + :favorite-project-infos="favoriteProjectInfos" /> @@ -27,6 +28,7 @@ export default { }, computed: mapGetters([ "namedProjectInfos", + "favoriteProjectInfos", "managedProject", "labels", "priorities", diff --git a/client/src/components/menu/ProjectListContainer.vue b/client/src/components/menu/ProjectListContainer.vue index 043c979b..ee246d71 100644 --- a/client/src/components/menu/ProjectListContainer.vue +++ b/client/src/components/menu/ProjectListContainer.vue @@ -6,7 +6,7 @@ 프로젝트 - + mdi-plus @@ -14,15 +14,16 @@ mdi-circle - + {{ project.title }} {{ project.taskCount }} state.currentProject, - namedProjectInfos: (state) => state.projectInfos.filter((project) => project.title !== "관리함"), + namedProjectInfos: (state) => + state.projectInfos.filter((project) => project.title !== "관리함" && !project.isFavorite), managedProject: (state) => state.projectInfos.find((project) => project.title === "관리함"), + favoriteProjectInfos: (state) => state.projectInfos.filter((project) => project.isFavorite), projectList: (state) => state.projectList, }; diff --git a/server/src/services/project.js b/server/src/services/project.js index c9dbf4b6..941d2a89 100644 --- a/server/src/services/project.js +++ b/server/src/services/project.js @@ -11,6 +11,7 @@ const retrieveProjects = async () => { 'id', 'title', 'color', + 'isFavorite', [sequelize.fn('COUNT', sequelize.col('tasks.id')), 'taskCount'], ], include: { From 667a0c03cc77e5290174c15c4fdfa30a860ca1cd Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 01:57:53 +0900 Subject: [PATCH 222/281] =?UTF-8?q?[FE]=20FEAT:=20favoriteProject=EC=97=90?= =?UTF-8?q?=20router=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/menu/FavoriteProjectList.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index e97ff0e3..8c000c1c 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -25,6 +25,7 @@ v-for="favoriteProjectInfo in favoriteProjectInfos" class="pl-8" :key="favoriteProjectInfo.id" + :to="`/project/${favoriteProjectInfo.id}`" > mdi-circle From eda8eea27b5d0d9837546c738cff6e36b2dfec9e Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 02:02:42 +0900 Subject: [PATCH 223/281] =?UTF-8?q?[FE]=20STYLE:=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20dialog=20max-height=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/views/Task.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/views/Task.vue b/client/src/views/Task.vue index 61753927..1e3331f9 100644 --- a/client/src/views/Task.vue +++ b/client/src/views/Task.vue @@ -50,3 +50,8 @@ export default { }, }; + From 2fd6cc23399b8dffda3a3b6c5ebdb90799a1650c Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 02:11:55 +0900 Subject: [PATCH 224/281] =?UTF-8?q?[FE]=20REFACTOR:=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20comment=20=EB=B6=84=EA=B8=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{task => comment}/CommentFormContainer.vue | 0 .../src/components/{task => comment}/CommentItem.vue | 2 +- .../src/components/{task => comment}/CommentList.vue | 5 ++--- client/src/components/task/ChildTaskList.vue | 4 ++-- client/src/components/task/TaskDetailContainer.vue | 4 ++-- client/src/components/task/TaskDetailTabs.vue | 2 +- client/src/store/comment.js | 12 ------------ client/src/store/task.js | 1 - 8 files changed, 8 insertions(+), 22 deletions(-) rename client/src/components/{task => comment}/CommentFormContainer.vue (100%) rename client/src/components/{task => comment}/CommentItem.vue (98%) rename client/src/components/{task => comment}/CommentList.vue (67%) diff --git a/client/src/components/task/CommentFormContainer.vue b/client/src/components/comment/CommentFormContainer.vue similarity index 100% rename from client/src/components/task/CommentFormContainer.vue rename to client/src/components/comment/CommentFormContainer.vue diff --git a/client/src/components/task/CommentItem.vue b/client/src/components/comment/CommentItem.vue similarity index 98% rename from client/src/components/task/CommentItem.vue rename to client/src/components/comment/CommentItem.vue index 19c1f275..45f3f2dc 100644 --- a/client/src/components/task/CommentItem.vue +++ b/client/src/components/comment/CommentItem.vue @@ -22,7 +22,7 @@ {{ comment.updatedAt }} {{ comment.content }} - +
mdi-pencil diff --git a/client/src/components/task/CommentList.vue b/client/src/components/comment/CommentList.vue similarity index 67% rename from client/src/components/task/CommentList.vue rename to client/src/components/comment/CommentList.vue index 15eee2e5..daf21e4a 100644 --- a/client/src/components/task/CommentList.vue +++ b/client/src/components/comment/CommentList.vue @@ -4,12 +4,11 @@ -
{{ this.comments }}
+ + diff --git a/client/src/store/project.js b/client/src/store/project.js index e01bbde5..5f29fb9e 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -57,7 +57,16 @@ const actions = { commit("SET_ERROR_ALERT", err.response); } }, + async updateProject({ dispatch, commit }, { projectId, data }) { + try { + await projectAPI.updateProject(projectId, data); + await dispatch("fetchProjectInfos"); + commit("SET_SUCCESS_ALERT", "프로젝트가 수정되었습니다."); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, async updateSectionTitle({ dispatch, commit }, { projectId, sectionId, title }) { try { const { data } = await projectAPI.updateSection(projectId, sectionId, { title }); diff --git a/server/src/services/project.js b/server/src/services/project.js index 941d2a89..1ee103d3 100644 --- a/server/src/services/project.js +++ b/server/src/services/project.js @@ -12,6 +12,7 @@ const retrieveProjects = async () => { 'title', 'color', 'isFavorite', + 'isList', [sequelize.fn('COUNT', sequelize.col('tasks.id')), 'taskCount'], ], include: { From 9a978638df953ee806609ebb4f746cf6e6d13180 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 05:03:50 +0900 Subject: [PATCH 228/281] =?UTF-8?q?[FE]=20REFACTOR:=20update=20project=20v?= =?UTF-8?q?alidation=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/project/UpdateProjectModal.vue | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/client/src/components/project/UpdateProjectModal.vue b/client/src/components/project/UpdateProjectModal.vue index 1bfb8f7c..fb73347d 100644 --- a/client/src/components/project/UpdateProjectModal.vue +++ b/client/src/components/project/UpdateProjectModal.vue @@ -93,6 +93,21 @@ export default { this.$emit("handleUpdateModal"); }, UpdateProject() { + if (this.title === "관리함") { + this.SET_ERROR_ALERT({ + data: { message: "해당 제목으로 프로젝트를 생성할 수 없습니다." }, + status: 406, + }); + this.title = ""; + return; + } + if (!this.color) { + this.SET_ERROR_ALERT({ + data: { message: "프로젝트 색상을 지정해주세요" }, + status: 406, + }); + return; + } this.updateProject({ projectId: this.projectInfo.id, data: { From 44194f843de48ca42d90dc0b24d3e33fa072f49f Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 05:18:41 +0900 Subject: [PATCH 229/281] =?UTF-8?q?[FE]=20FEAT:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/project.js | 3 + .../components/menu/ProjectListContainer.vue | 19 ++++++- .../components/project/DeleteProjectModal.vue | 56 +++++++++++++++++++ client/src/store/project.js | 10 ++++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 client/src/components/project/DeleteProjectModal.vue diff --git a/client/src/api/project.js b/client/src/api/project.js index 5664fba5..dfc953b4 100644 --- a/client/src/api/project.js +++ b/client/src/api/project.js @@ -22,6 +22,9 @@ const projectAPI = { updateTaskPosition(projectId, sectionId, data) { return myAxios.patch(`/project/${projectId}/section/${sectionId}/position`, data); }, + deleteProject(projectId) { + return myAxios.delete(`/project/${projectId}`); + }, }; export default projectAPI; diff --git a/client/src/components/menu/ProjectListContainer.vue b/client/src/components/menu/ProjectListContainer.vue index 578e68f4..14e276de 100644 --- a/client/src/components/menu/ProjectListContainer.vue +++ b/client/src/components/menu/ProjectListContainer.vue @@ -39,10 +39,10 @@ - 프로젝트 수정 + 프로젝트 수정 - - 프로젝트 삭제 + + 프로젝트 삭제 @@ -69,12 +69,19 @@ v-on:handleUpdateModal="updateDialog = !updateDialog" :projectInfo="projectInfos.find((project) => project.id === projectId)" /> + diff --git a/client/src/components/project/DeleteProjectModal.vue b/client/src/components/project/DeleteProjectModal.vue new file mode 100644 index 00000000..ff15affa --- /dev/null +++ b/client/src/components/project/DeleteProjectModal.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/client/src/store/project.js b/client/src/store/project.js index 5f29fb9e..63665efc 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -67,6 +67,16 @@ const actions = { commit("SET_ERROR_ALERT", err.response); } }, + async deleteProject({ dispatch, commit }, { projectId }) { + try { + await projectAPI.deleteProject(projectId); + + await dispatch("fetchProjectInfos"); + commit("SET_SUCCESS_ALERT", "프로젝트가 삭제되었습니다."); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, async updateSectionTitle({ dispatch, commit }, { projectId, sectionId, title }) { try { const { data } = await projectAPI.updateSection(projectId, sectionId, { title }); From 436df361bf990ba0be7365dfe49c359bf8abf22c Mon Sep 17 00:00:00 2001 From: shkilo Date: Tue, 8 Dec 2020 10:27:33 +0900 Subject: [PATCH 230/281] =?UTF-8?q?[BE]=20FEAT:=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20position=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/services/section.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/services/section.js b/server/src/services/section.js index c7f6ca09..5415c97b 100644 --- a/server/src/services/section.js +++ b/server/src/services/section.js @@ -6,8 +6,18 @@ const sectionModel = models.section; const create = async ({ projectId, ...data }) => { const result = await sequelize.transaction(async t => { - const project = await models.project.findByPk(projectId); - const section = await sectionModel.create(data, { transaction: t }); + const project = await models.project.findByPk(projectId, { + include: sectionModel, + }); + + const maxPosition = project.toJSON().sections.reduce((max, section) => { + return Math.max(max, section.position); + }, 0); + + const section = await sectionModel.create( + { ...data, position: maxPosition + 1 }, + { transaction: t }, + ); await section.setProject(project, { transaction: t, }); From 8a908d15faade62b1a15e78edec2ca3728c1b63a Mon Sep 17 00:00:00 2001 From: shkilo Date: Tue, 8 Dec 2020 10:28:31 +0900 Subject: [PATCH 231/281] =?UTF-8?q?[FE]=20FEAT:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EC=96=B4=20getter,=20ac?= =?UTF-8?q?tion=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/project.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/client/src/store/project.js b/client/src/store/project.js index 3c9cd1c4..ae3aec53 100644 --- a/client/src/store/project.js +++ b/client/src/store/project.js @@ -19,6 +19,7 @@ const state = { const getters = { currentProject: (state) => state.currentProject, todayProject: (state) => state.todayProject, + projectInfos: (state) => state.projectInfos, namedProjectInfos: (state) => state.projectInfos.filter((project) => project.title !== "관리함"), managedProject: (state) => state.projectInfos.find((project) => project.title === "관리함"), projectList: (state) => state.projectList, @@ -59,6 +60,22 @@ const actions = { } }, + async addSection({ dispatch, commit }, { projectId, section }) { + try { + const { data } = await projectAPI.createSection(projectId, { + title: section.title, + }); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", projectId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async updateSectionTitle({ dispatch, commit }, { projectId, sectionId, title }) { try { const { data } = await projectAPI.updateSection(projectId, sectionId, { title }); From ad2a9323d1e9a2d6e4567fc612388cab960c7f9d Mon Sep 17 00:00:00 2001 From: shkilo Date: Tue, 8 Dec 2020 10:29:26 +0900 Subject: [PATCH 232/281] =?UTF-8?q?[FE]=20FEAT:=20=ED=95=A0=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/project/TaskItem.vue | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/client/src/components/project/TaskItem.vue b/client/src/components/project/TaskItem.vue index 449bf753..5c76203a 100644 --- a/client/src/components/project/TaskItem.vue +++ b/client/src/components/project/TaskItem.vue @@ -5,6 +5,7 @@ @dragover.prevent="handleDragOver" @drop.prevent="handleDrop" :class="{ dragging: task.dragging }" + class="task-item text-subtitle" > @@ -18,7 +19,11 @@
- {{ task.title }} + + + {{ task.title }} + +
@@ -27,6 +32,7 @@ From 45653ac0abc00ba7a46a43c734cf45960e0faab5 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 14:16:35 +0900 Subject: [PATCH 234/281] =?UTF-8?q?[FE]=20FEAT:=20=EC=A6=90=EA=B2=A8?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/menu/FavoriteProjectList.vue | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index 5cad32ed..00991162 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -25,7 +25,7 @@ v-for="favoriteProjectInfo in favoriteProjectInfos" class="pl-4" :key="favoriteProjectInfo.id" - :to="`/project/${favoriteProjectInfo.id}`" + @click="pushRoute(favoriteProjectInfo.id)" > mdi-circle @@ -35,18 +35,70 @@ {{ favoriteProjectInfo.title }} {{ favoriteProjectInfo.taskCount }}
+ + + + + 프로젝트 수정 + + + 프로젝트 삭제 + + + + + From c5a7739ce15a95b945f9a0f1b626e7109ee58ce7 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Tue, 8 Dec 2020 14:27:20 +0900 Subject: [PATCH 235/281] =?UTF-8?q?[FE]=20REFACTOR:=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=20padding=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/menu/FavoriteProjectList.vue | 2 +- client/src/components/menu/ProjectListContainer.vue | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index 00991162..a96f1ba6 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -37,7 +37,7 @@
@@ -21,6 +29,7 @@ import { mapActions } from "vuex"; import ProjectContainerHeader from "./ProjectContainerHeader"; import SectionContainer from "@/components/project/SectionContainer"; +import AddSection from "@/components/project/AddSection"; export default { props: { @@ -29,6 +38,7 @@ export default { data() { return { boardView: false, + showAddSection: false, }; }, methods: { @@ -39,18 +49,26 @@ export default { changeToBoardView() { this.boardView = true; }, + toggleAddSection() { + this.showAddSection = !this.showAddSection; + }, }, components: { SectionContainer, ProjectContainerHeader, + AddSection, }, }; From 6488cda7db3e376cc0224593901f263e43736eef Mon Sep 17 00:00:00 2001 From: shkilo Date: Tue, 8 Dec 2020 19:18:43 +0900 Subject: [PATCH 240/281] =?UTF-8?q?[BE]=20REFACTOR:=20dto=20validation=20?= =?UTF-8?q?=EC=85=8B=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package.json | 1 + server/.babelrc.js | 6 +++ server/package.json | 7 +++ server/src/controllers/task.js | 20 +++++--- server/src/models/dto/task.js | 93 ++++++++++++++++++++++++++++++++++ server/src/utils/validator.js | 9 ++++ 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 server/.babelrc.js create mode 100644 server/src/models/dto/task.js create mode 100644 server/src/utils/validator.js diff --git a/client/package.json b/client/package.json index f736fe03..3d3125c1 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "axios": "^0.21.0", "core-js": "^3.6.5", "vue": "^2.6.11", + "vue-markdown": "^2.2.4", "vue-router": "^3.2.0", "vuetify": "^2.2.11", "vuex": "^3.4.0" diff --git a/server/.babelrc.js b/server/.babelrc.js new file mode 100644 index 00000000..fd6ec5f3 --- /dev/null +++ b/server/.babelrc.js @@ -0,0 +1,6 @@ +const plugins = [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-proposal-class-properties', { loose: true }], +]; + +module.exports = { plugins }; diff --git a/server/package.json b/server/package.json index 0dbdacb8..3f4f6107 100644 --- a/server/package.json +++ b/server/package.json @@ -28,6 +28,12 @@ } }, "dependencies": { + "@babel/cli": "^7.12.8", + "@babel/core": "^7.12.3", + "@babel/node": "^7.12.6", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "class-transformer": "^0.3.1", + "class-validator": "^0.12.2", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "~4.16.1", @@ -44,6 +50,7 @@ "uuid": "^8.3.1" }, "devDependencies": { + "@babel/plugin-proposal-decorators": "^7.12.1", "concurrently": "^5.3.0", "eslint": "^7.13.0", "eslint-config-airbnb-base": "^14.2.0", diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index b8d5ab3b..80f8055d 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -1,7 +1,8 @@ +const TaskDto = require('@models/dto/task'); const taskService = require('@services/task'); +const validator = require('@utils/validator'); const { asyncTryCatch } = require('@utils/async-try-catch'); const { responseHandler } = require('@utils/handler'); -const { isValidDueDate } = require('@utils/date'); const getTaskById = asyncTryCatch(async (req, res) => { const task = await taskService.retrieveById(req.params.taskId); @@ -16,16 +17,21 @@ const getAllTasks = asyncTryCatch(async (req, res) => { }); const createTask = asyncTryCatch(async (req, res) => { - const { dueDate } = req.body; - - // TODO middle ware로 빼내는게 좋을 것 같음 - if (!isValidDueDate(dueDate)) { - const err = new Error('유효하지 않은 dueDate'); + const { projectId, sectionId } = req.params; + const task = { + ...req.body, + projectId, + sectionId, + }; + + try { + await validator(TaskDto, task); + } catch (errs) { + const err = new Error('Bad Request'); err.status = 400; throw err; } - const { projectId, sectionId } = req.params; await taskService.create({ projectId, sectionId, ...req.body }); responseHandler(res, 201, { message: 'ok' }); }); diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js new file mode 100644 index 00000000..eb139259 --- /dev/null +++ b/server/src/models/dto/task.js @@ -0,0 +1,93 @@ +const { + validate, + validateOrReject, + Contains, + IsInt, + Length, + IsEmail, + IsFQDN, + IsDate, + Min, + Max, + IsString, + IsDefined, + IsNotEmpty, + IsBoolean, + IsArray, + MinDate, +} = require('class-validator'); + +class TaskDto { + @IsString() + @Length(36, 36) + @IsNotEmpty() + id; + + @IsString() + @IsNotEmpty() + title; + + // TODO: custom validation decorator + @IsDate() + @MinDate(new Date('2020-12-08')) + dueDate; + + @IsInt() + @IsNotEmpty() + position; + + @IsBoolean() + @IsNotEmpty() + isDone; + + @IsString() + @Length(36, 36) + parentId; + + @IsString() + @Length(36, 36) + sectionId; + + @IsString() + @Length(36, 36) + projectId; + + @IsString() + @Length(36, 36) + priorityId; + + @Length(36, 36) + alarmId; + + @IsArray + orderedTasks; + + // constructor({ id, title, dueDate, position, isDone }) { + // this.id = id; + // this.title = title; + // this.dueDate = dueDate; + // this.position = position; + // this.isDone = isDone; + // } +} + +// const task = new TaskDto(); +// task.id = 'ff4dd832-1567-4d74-b41d-bd85e96ce329'; +// task.title = 'zkzkzk'; +// task.dueDate = new Date('2020-12-07'); +// task.position = 4; +// task.isDone = true; +// task.parentId = 'ff4dd832-1567-4d74-b41d-bd85e96ce329'; + +// await validateOrReject(task, { gropus }).catch(errors => { +// console.log('Promise rejected (validation failed). Errors: ', errors); +// }); + +// sectionId; + +// projectId +// priorityId; +// alarmId; +// orderedTasks + +module.exports = TaskDto; diff --git a/server/src/utils/validator.js b/server/src/utils/validator.js new file mode 100644 index 00000000..dede085f --- /dev/null +++ b/server/src/utils/validator.js @@ -0,0 +1,9 @@ +const { validateOrReject } = require('class-validator'); +const { plainToClass } = require('class-transformer'); + +const validator = async (Dto, object, options) => { + const classObject = plainToClass(Dto, object); + await validateOrReject(classObject, options); +}; + +module.exports = validator; From c5feaf03dc477c0beacf0795e7d1c5ca6b142e44 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 22:43:10 +0900 Subject: [PATCH 241/281] =?UTF-8?q?[BE]=20CHORE:=20run=20dev=20script=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index 3f4f6107..95307003 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "start": "node ./src/app.js", - "dev": "nodemon ./src/app.js", + "dev": "nodemon --exec babel-node ./src/app.js", "seed": "npx sequelize-cli db:seed:all", "unseed": "npx sequelize-cli db:seed:undo:all", "test": "jest --detectOpenHandles --forceExit", From 488b8bcb6961c085435475af49aa034e4f882045 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 22:44:21 +0900 Subject: [PATCH 242/281] =?UTF-8?q?[BE]=20FEAT:=20validation=20Error=20mes?= =?UTF-8?q?sage=20=EC=B6=94=EC=B6=9C=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/utils/validator.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/utils/validator.js b/server/src/utils/validator.js index dede085f..9e93f8e8 100644 --- a/server/src/utils/validator.js +++ b/server/src/utils/validator.js @@ -3,7 +3,14 @@ const { plainToClass } = require('class-transformer'); const validator = async (Dto, object, options) => { const classObject = plainToClass(Dto, object); - await validateOrReject(classObject, options); + await validateOrReject(classObject, { ...options, stopAtFirstError: true }); }; -module.exports = validator; +const getErrorMsg = errorArray => { + const [validationError] = errorArray; + const [message] = Object.values(validationError.constraints); + + return message; +}; + +module.exports = { validator, getErrorMsg }; From 1df6b3cece362e1f8f296b6edc17118b68864b27 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 22:48:17 +0900 Subject: [PATCH 243/281] =?UTF-8?q?[BE]=20FEAT:=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=EB=A5=BC=20yyyy-MM-dd=EB=A1=9C=20return?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/utils/date.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/src/utils/date.js b/server/src/utils/date.js index 609f1bf6..ed92b1e7 100644 --- a/server/src/utils/date.js +++ b/server/src/utils/date.js @@ -12,4 +12,13 @@ const isValidDueDate = inputDate => { ); }; -module.exports = { isValidDueDate }; +const getTodayString = () => { + const today = new Date(); + const dd = String(today.getDate()).padStart(2, '0'); + const mm = String(today.getMonth() + 1).padStart(2, '0'); // January is 0! + const yyyy = today.getFullYear(); + + return `${yyyy}-${mm}-${dd}`; +}; + +module.exports = { isValidDueDate, getTodayString }; From c4f07eda050c81a7d50230afa178c5465dfcf7c5 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Tue, 8 Dec 2020 23:21:27 +0900 Subject: [PATCH 244/281] =?UTF-8?q?[BE]=20FEAT:=20task=20dto=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=83=9D=EC=84=B1=20validation=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/task.js | 13 ++++----- server/src/models/dto/error-messages.js | 6 ++++ server/src/models/dto/task.js | 37 +++++++++++++++---------- server/src/utils/date.js | 11 +------- server/src/utils/validator.js | 21 ++++++++++++-- 5 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 server/src/models/dto/error-messages.js diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index 80f8055d..8015b91f 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -1,6 +1,6 @@ const TaskDto = require('@models/dto/task'); const taskService = require('@services/task'); -const validator = require('@utils/validator'); +const { validator, getErrorMsg } = require('@utils/validator'); const { asyncTryCatch } = require('@utils/async-try-catch'); const { responseHandler } = require('@utils/handler'); @@ -18,16 +18,13 @@ const getAllTasks = asyncTryCatch(async (req, res) => { const createTask = asyncTryCatch(async (req, res) => { const { projectId, sectionId } = req.params; - const task = { - ...req.body, - projectId, - sectionId, - }; + const task = { ...req.body, projectId, sectionId }; try { - await validator(TaskDto, task); + await validator(TaskDto, task, { groups: ['create'] }); } catch (errs) { - const err = new Error('Bad Request'); + const message = getErrorMsg(errs); + const err = new Error(message); err.status = 400; throw err; } diff --git a/server/src/models/dto/error-messages.js b/server/src/models/dto/error-messages.js new file mode 100644 index 00000000..05175219 --- /dev/null +++ b/server/src/models/dto/error-messages.js @@ -0,0 +1,6 @@ +const missingProperty = property => `${property}를 입력해주세요`; +const wrongProperty = property => `${property}값이 올바르지 않습니다`; + +const beforeDueDate = 'dueDate는 현재시간보다 이전을 설정할 수 없습니다.'; + +module.exports = { missingProperty, wrongProperty, beforeDueDate }; diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js index eb139259..1fade6e3 100644 --- a/server/src/models/dto/task.js +++ b/server/src/models/dto/task.js @@ -14,22 +14,29 @@ const { IsNotEmpty, IsBoolean, IsArray, - MinDate, + MinLength, + IsDateString, + ValidateIf, + IsUUID, } = require('class-validator'); +const errorMessage = require('@models/dto/error-messages'); +const { isAfterToday } = require('@utils/validator'); class TaskDto { + @ValidateIf(o => !!o.id) @IsString() - @Length(36, 36) - @IsNotEmpty() + @IsUUID('4') id; - @IsString() - @IsNotEmpty() + @IsString({ groups: ['create'] }, { message: errorMessage.wrongProperty('title') }) + @MinLength(1, { groups: ['create'] }, { message: errorMessage.wrongProperty('title') }) title; - // TODO: custom validation decorator - @IsDate() - @MinDate(new Date('2020-12-08')) + @IsDateString( + { strict: true }, + { groups: ['create'], message: errorMessage.wrongProperty('dueDate') }, + ) + @isAfterToday('dueDate', { groups: ['create'], message: errorMessage.beforeDueDate }) dueDate; @IsInt() @@ -41,22 +48,22 @@ class TaskDto { isDone; @IsString() - @Length(36, 36) + @IsUUID('4') parentId; - @IsString() - @Length(36, 36) + @IsString({ groups: ['create'], message: errorMessage.wrongProperty('sectionId') }) + @IsUUID('4', { groups: ['create'], message: errorMessage.wrongProperty('sectionId') }) sectionId; - @IsString() - @Length(36, 36) + @IsString({ groups: ['create'], message: errorMessage.wrongProperty('projectId') }) + @IsUUID('4', { groups: ['create'], message: errorMessage.wrongProperty('projectId') }) projectId; @IsString() - @Length(36, 36) + @IsUUID('4') priorityId; - @Length(36, 36) + @IsUUID('4') alarmId; @IsArray diff --git a/server/src/utils/date.js b/server/src/utils/date.js index ed92b1e7..609f1bf6 100644 --- a/server/src/utils/date.js +++ b/server/src/utils/date.js @@ -12,13 +12,4 @@ const isValidDueDate = inputDate => { ); }; -const getTodayString = () => { - const today = new Date(); - const dd = String(today.getDate()).padStart(2, '0'); - const mm = String(today.getMonth() + 1).padStart(2, '0'); // January is 0! - const yyyy = today.getFullYear(); - - return `${yyyy}-${mm}-${dd}`; -}; - -module.exports = { isValidDueDate, getTodayString }; +module.exports = { isValidDueDate }; diff --git a/server/src/utils/validator.js b/server/src/utils/validator.js index 9e93f8e8..1c1592d8 100644 --- a/server/src/utils/validator.js +++ b/server/src/utils/validator.js @@ -1,5 +1,6 @@ -const { validateOrReject } = require('class-validator'); +const { validateOrReject, registerDecorator } = require('class-validator'); const { plainToClass } = require('class-transformer'); +const { isValidDueDate } = require('@utils/date'); const validator = async (Dto, object, options) => { const classObject = plainToClass(Dto, object); @@ -12,5 +13,21 @@ const getErrorMsg = errorArray => { return message; }; +const isAfterToday = (property, validationOptions) => { + return (object, propertyName) => { + registerDecorator({ + name: 'isAfterToday', + target: object.constructor, + propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value, args) { + return isValidDueDate(value); // you can return a Promise here as well, if you want to make async validation + }, + }, + }); + }; +}; -module.exports = { validator, getErrorMsg }; +module.exports = { validator, getErrorMsg, isAfterToday }; From a8db9911a680046f89a71d375844c07566803819 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 01:35:28 +0900 Subject: [PATCH 245/281] =?UTF-8?q?[BE]=20TEST:=20task=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20api=20=EB=B6=84=EA=B8=B0=20&=20validation=20test=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/project.api.test.js | 170 --------------- server/test/project.task.api.test.js | 300 +++++++++++++++++++++++++++ 2 files changed, 300 insertions(+), 170 deletions(-) create mode 100644 server/test/project.task.api.test.js diff --git a/server/test/project.api.test.js b/server/test/project.api.test.js index 570ec9b4..b61aa48c 100644 --- a/server/test/project.api.test.js +++ b/server/test/project.api.test.js @@ -238,173 +238,3 @@ describe('delete section', () => { done(); }); }); - -describe('post task', () => { - it('일반 task 생성', async done => { - // given - const expectedProjectId = seeder.projects[0].id; - const expectedSectionId = seeder.sections[0].id; - const newTask = { - title: '할일', - labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), - priorityId: seeder.priorities[0].id, - dueDate: new Date(), - parentId: null, - alarmId: seeder.alarms[0].id, - position: 1, - }; - - // when - const res = await request(app) - .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) - .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); - - // then - expect(res.status).toBe(status.SUCCESS.POST.CODE); - expect(res.body.message).toBe(status.SUCCESS.MSG); - done(); - }); - - it('project 없이 생성', async done => { - // TODO: validation 체크하는 로직 생각해서 테스트 코드 작성해야 합니다. - // given - - // when - - // then - expect(true).toBeFalsy(); - done(); - }); - - it('label 없이 생성', async done => { - // given - const expectedProjectId = seeder.projects[0].id; - const expectedSectionId = seeder.sections[0].id; - const newTask = { - title: '할일', - labelIdList: JSON.stringify([]), - priorityId: seeder.priorities[1].id, - dueDate: new Date(), - parentId: null, - alarmId: seeder.alarms[0].id, - position: 1, - }; - - // when - const res = await request(app) - .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) - .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); - - // then - expect(res.status).toBe(status.SUCCESS.POST.CODE); - expect(res.body.message).toBe(status.SUCCESS.MSG); - done(); - }); - - it('priority 없이 생성', async done => { - // given - const expectedProjectId = seeder.projects[0].id; - const expectedSectionId = seeder.sections[0].id; - const newTask = { - title: '할일', - labelIdList: JSON.stringify([]), - priorityId: null, - dueDate: new Date(), - parentId: null, - alarmId: seeder.alarms[0].id, - position: 1, - }; - - // when - const res = await request(app) - .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) - .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); - - // then - expect(res.status).toBe(status.SUCCESS.POST.CODE); - expect(res.body.message).toBe(status.SUCCESS.MSG); - done(); - }); - - it('하위 할일 생성', async done => { - // given - const expectedProjectId = seeder.projects[0].id; - const expectedSectionId = seeder.sections[0].id; - const newTask = { - title: '할일', - labelIdList: JSON.stringify([]), - priorityId: seeder.priorities[1].id, - dueDate: new Date(), - parentId: seeder.tasks[0].id, - alarmId: seeder.alarms[0].id, - position: 1, - }; - - // when - const res = await request(app) - .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) - .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); - - // then - expect(res.status).toBe(status.SUCCESS.POST.CODE); - expect(res.body.message).toBe(status.SUCCESS.MSG); - done(); - }); - - it('alarm 없이 생성', async done => { - // given - const expectedProjectId = seeder.projects[0].id; - const expectedSectionId = seeder.sections[0].id; - const newTask = { - title: '할일', - labelIdList: JSON.stringify([]), - priorityId: seeder.priorities[1].id, - dueDate: new Date(), - parentId: seeder.tasks[0].id, - alarmId: null, - position: 1, - }; - - // when - const res = await request(app) - .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) - .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); - - // then - expect(res.status).toBe(status.SUCCESS.POST.CODE); - expect(res.body.message).toBe(status.SUCCESS.MSG); - done(); - }); - - it('유요하지 않은 duedate 생성', async done => { - // given - const expectedProjectId = seeder.projects[0].id; - const expectedSectionId = seeder.sections[0].id; - const newTask = { - title: '할일', - projectId: seeder.projects[1].id, - labelIdList: JSON.stringify([]), - priorityId: seeder.priorities[1].id, - dueDate: '2020-10-28', - parentId: seeder.tasks[0].id, - alarmId: seeder.alarms[0].id, - position: 1, - }; - - // when - const res = await request(app) - .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) - .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); - - // then - expect(res.status).toBe(status.BAD_REQUEST.CODE); - expect(res.body.message).toBe('유효하지 않은 dueDate'); - done(); - }); -}); diff --git a/server/test/project.task.api.test.js b/server/test/project.task.api.test.js new file mode 100644 index 00000000..7efc0b47 --- /dev/null +++ b/server/test/project.task.api.test.js @@ -0,0 +1,300 @@ +require('module-alias/register'); +const request = require('supertest'); +const app = require('@root/app'); +const seeder = require('@test/test-seed'); +const status = require('@test/response-status'); +const { createJWT } = require('@utils/auth'); +const errorMessage = require('@models/dto/error-messages'); + +beforeAll(async done => { + await seeder.up(); + done(); +}); + +afterAll(async done => { + await seeder.down(); + done(); +}); + +describe('post task', () => { + it('성공하는 task 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('label 없이 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('priority 없이 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: null, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('하위 할일 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: seeder.tasks[0].id, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('alarm 없이 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: seeder.tasks[0].id, + alarmId: null, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('유효하지 않은 duedate 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + projectId: seeder.projects[1].id, + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: '2020-10-28', + parentId: seeder.tasks[0].id, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.DUEDATE_ERROR); + done(); + }); + + it('잘못된 projectId 생성', async done => { + // given + const expectedProjectId = 'wrongId'; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('projectId')); + done(); + }); + it('잘못된 sectionId 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = 'wrongId'; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('sectionId')); + done(); + }); + it('잘못된 parentId 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const expectedParentId = 'wrongId'; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: expectedParentId, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('parentId')); + done(); + }); + it('작업 id가 포함된 생성', async done => { + // given + const expectedId = seeder.projects[1].id; + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + id: expectedId, + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.UNNECESSARY_INPUT_ERROR('id')); + done(); + }); + it('빈 문자열 title 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('title')); + done(); + }); +}); From ef77d868f3665f8ce108cf04244d480ad0ea0ff8 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 01:36:10 +0900 Subject: [PATCH 246/281] =?UTF-8?q?[BE]=20FEAT:=20TEST=20CODE=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20task=20dto=20=EA=B5=AC=ED=98=84=20(task=20?= =?UTF-8?q?create)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/dto/task.js | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js index 1fade6e3..f8e3022f 100644 --- a/server/src/models/dto/task.js +++ b/server/src/models/dto/task.js @@ -1,16 +1,6 @@ const { - validate, - validateOrReject, - Contains, IsInt, - Length, - IsEmail, - IsFQDN, - IsDate, - Min, - Max, IsString, - IsDefined, IsNotEmpty, IsBoolean, IsArray, @@ -18,25 +8,27 @@ const { IsDateString, ValidateIf, IsUUID, + IsOptional, + IsEmpty, } = require('class-validator'); const errorMessage = require('@models/dto/error-messages'); const { isAfterToday } = require('@utils/validator'); class TaskDto { - @ValidateIf(o => !!o.id) + @IsEmpty({ groups: ['create'], message: errorMessage.UNNECESSARY_INPUT_ERROR('id') }) @IsString() @IsUUID('4') id; - @IsString({ groups: ['create'] }, { message: errorMessage.wrongProperty('title') }) - @MinLength(1, { groups: ['create'] }, { message: errorMessage.wrongProperty('title') }) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('title') }) + @MinLength(1, { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('title') }) title; @IsDateString( { strict: true }, - { groups: ['create'], message: errorMessage.wrongProperty('dueDate') }, + { groups: ['create'], message: errorMessage.TYPE_ERROR('dueDate') }, ) - @isAfterToday('dueDate', { groups: ['create'], message: errorMessage.beforeDueDate }) + @isAfterToday('dueDate', { groups: ['create'], message: errorMessage.DUEDATE_ERROR }) dueDate; @IsInt() @@ -47,16 +39,17 @@ class TaskDto { @IsNotEmpty() isDone; - @IsString() - @IsUUID('4') + @IsOptional({ groups: ['create'] }) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('sectionId') }) + @IsUUID('4', { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('parentId') }) parentId; - @IsString({ groups: ['create'], message: errorMessage.wrongProperty('sectionId') }) - @IsUUID('4', { groups: ['create'], message: errorMessage.wrongProperty('sectionId') }) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('sectionId') }) + @IsUUID('4', { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('sectionId') }) sectionId; - @IsString({ groups: ['create'], message: errorMessage.wrongProperty('projectId') }) - @IsUUID('4', { groups: ['create'], message: errorMessage.wrongProperty('projectId') }) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('projectId') }) + @IsUUID('4', { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('projectId') }) projectId; @IsString() From 31a04650cd3a315c7d3289c3ae31c5652e076d7b Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 01:36:41 +0900 Subject: [PATCH 247/281] =?UTF-8?q?[BE]=20REFACTOR:=20error-message=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/dto/error-messages.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/models/dto/error-messages.js b/server/src/models/dto/error-messages.js index 05175219..4f5b2459 100644 --- a/server/src/models/dto/error-messages.js +++ b/server/src/models/dto/error-messages.js @@ -1,6 +1,6 @@ -const missingProperty = property => `${property}를 입력해주세요`; -const wrongProperty = property => `${property}값이 올바르지 않습니다`; - -const beforeDueDate = 'dueDate는 현재시간보다 이전을 설정할 수 없습니다.'; - -module.exports = { missingProperty, wrongProperty, beforeDueDate }; +module.exports = { + TYPE_ERROR: property => `${property} 타입이 올바르지 않습니다.`, + UNNECESSARY_INPUT_ERROR: property => `불필요한 값이 포함되어 있습니다. => ${property}`, + INVALID_INPUT_ERROR: property => `${property}값이 올바르지 않습니다.`, + DUEDATE_ERROR: 'dueDate는 현재시간보다 이전을 설정할 수 없습니다.', +}; From 2dad6a2f3b9844cb2c4897470dfda341becf1ecd Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 01:46:30 +0900 Subject: [PATCH 248/281] =?UTF-8?q?[BE]=20REFACTOR:=20DTO=20error-message?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/dto/error-messages.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/server/src/models/dto/error-messages.js b/server/src/models/dto/error-messages.js index 05175219..51fbe62b 100644 --- a/server/src/models/dto/error-messages.js +++ b/server/src/models/dto/error-messages.js @@ -1,6 +1,5 @@ -const missingProperty = property => `${property}를 입력해주세요`; -const wrongProperty = property => `${property}값이 올바르지 않습니다`; - -const beforeDueDate = 'dueDate는 현재시간보다 이전을 설정할 수 없습니다.'; - -module.exports = { missingProperty, wrongProperty, beforeDueDate }; +module.exports = { + TYPE_ERROR: property => `${property}를 입력해주세요`, + INVALID_INPUT_ERROR: property => `${property}값이 올바르지 않습니다`, + DUEDATE_ERROR: () => 'dueDate는 현재시간보다 이전을 설정할 수 없습니다.', +}; From 06416374a230b03e9d43e800e3105a3003cc4145 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 01:47:38 +0900 Subject: [PATCH 249/281] =?UTF-8?q?[BE]=20FEAT:=20project=20DTO=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/dto/project.js | 37 ++++++++++++++++++++++++++++++++ server/src/models/dto/task.js | 16 +++++++------- 2 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 server/src/models/dto/project.js diff --git a/server/src/models/dto/project.js b/server/src/models/dto/project.js new file mode 100644 index 00000000..734fba35 --- /dev/null +++ b/server/src/models/dto/project.js @@ -0,0 +1,37 @@ +const { + IsString, + IsNotEmpty, + ValidateIf, + IsHexColor, + IsUUID, + MinLength, + IsBoolean, +} = require('class-validator'); +const errorMessage = require('@models/dto/error-messages'); + +class ProjectDto { + @ValidateIf(o => !!o.id) + @IsString() + @IsUUID('4') + id; + + @ValidateIf(o => !!o.title) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('제목') }) + @MinLength(1, { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('제목') }) + title; + + @ValidateIf(o => !!o.color) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('색상') }) + @IsHexColor({ groups: ['created'], message: errorMessage.INVALID_INPUT_ERROR('색상') }) + color; + + @ValidateIf(o => !!o.isList) + @IsBoolean({ groups: ['create'], message: errorMessage.TYPE_ERROR('목록') }) + isList; + + @ValidateIf(o => !!o.isFavorite) + @IsBoolean({ message: errorMessage.TYPE_ERROR('즐겨찾기') }) + isFavorite; +} + +module.exports = ProjectDto; diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js index 1fade6e3..03fb834b 100644 --- a/server/src/models/dto/task.js +++ b/server/src/models/dto/task.js @@ -28,15 +28,15 @@ class TaskDto { @IsUUID('4') id; - @IsString({ groups: ['create'] }, { message: errorMessage.wrongProperty('title') }) - @MinLength(1, { groups: ['create'] }, { message: errorMessage.wrongProperty('title') }) + @IsString({ groups: ['create'] }, { message: errorMessage.TYPE_ERROR('title') }) + @MinLength(1, { groups: ['create'] }, { message: errorMessage.INVALID_INPUT_ERROR('title') }) title; @IsDateString( { strict: true }, - { groups: ['create'], message: errorMessage.wrongProperty('dueDate') }, + { groups: ['create'], message: errorMessage.TYPE_ERROR('dueDate') }, ) - @isAfterToday('dueDate', { groups: ['create'], message: errorMessage.beforeDueDate }) + @isAfterToday('dueDate', { groups: ['create'], message: errorMessage.DUEDATE_ERROR() }) dueDate; @IsInt() @@ -51,12 +51,12 @@ class TaskDto { @IsUUID('4') parentId; - @IsString({ groups: ['create'], message: errorMessage.wrongProperty('sectionId') }) - @IsUUID('4', { groups: ['create'], message: errorMessage.wrongProperty('sectionId') }) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('sectionId') }) + @IsUUID('4', { groups: ['create'], message: errorMessage.TYPE_ERROR('sectionId') }) sectionId; - @IsString({ groups: ['create'], message: errorMessage.wrongProperty('projectId') }) - @IsUUID('4', { groups: ['create'], message: errorMessage.wrongProperty('projectId') }) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('projectId') }) + @IsUUID('4', { groups: ['create'], message: errorMessage.TYPE_ERROR('projectId') }) projectId; @IsString() From 29358e210189bfd727ab661710a5f4fb08e130ad Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 01:48:02 +0900 Subject: [PATCH 250/281] =?UTF-8?q?[BE]=20FEAT:=20controller=EC=97=90=20DT?= =?UTF-8?q?O=20validation=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/project.js | 34 +++++++++++++++++++++++++++++-- server/src/services/project.js | 10 +++------ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/server/src/controllers/project.js b/server/src/controllers/project.js index f75bea6b..c5df9c27 100644 --- a/server/src/controllers/project.js +++ b/server/src/controllers/project.js @@ -1,6 +1,8 @@ +const ProjectDto = require('@models/dto/project'); const projectService = require('@services/project'); const { responseHandler } = require('@utils/handler'); const { asyncTryCatch } = require('@utils/async-try-catch'); +const { validator, getErrorMsg } = require('@utils/validator'); const getProjects = asyncTryCatch(async (req, res) => { const projects = await projectService.retrieveProjects(); @@ -22,19 +24,47 @@ const getProjectById = asyncTryCatch(async (req, res) => { const createProject = asyncTryCatch(async (req, res) => { const { id: creatorId } = req.user; + + try { + await validator(ProjectDto, req.body, { groups: ['create'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + const projectId = await projectService.create({ creatorId, ...req.body }); responseHandler(res, 201, { message: 'ok', projectId }); }); const updateProject = asyncTryCatch(async (req, res) => { - await projectService.update({ projectId: req.params.projectId, ...req.body }); + try { + await validator(ProjectDto, { id: req.params.projectId, ...req.body }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + await projectService.update({ id: req.params.projectId, ...req.body }); responseHandler(res, 200, { message: 'ok' }); }); const deleteProject = asyncTryCatch(async (req, res) => { - await projectService.remove(req.params.projectId); + try { + await validator(ProjectDto, { id: req.params.projectId }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + await projectService.remove({ id: req.params.projectId }); responseHandler(res, 200, { message: 'ok' }); }); diff --git a/server/src/services/project.js b/server/src/services/project.js index 1ee103d3..21f2f2c2 100644 --- a/server/src/services/project.js +++ b/server/src/services/project.js @@ -105,17 +105,13 @@ const findOrCreate = async data => { return await create(data); }; -const update = async ({ projectId, ...data }) => { - const result = await projectModel.update(data, { - where: { - id: projectId, - }, - }); +const update = async ({ id, ...data }) => { + const result = await projectModel.update(data, { where: { id } }); return result === 1; }; -const remove = async id => { +const remove = async ({ id }) => { const result = await projectModel.destroy({ where: { id } }); return result === 1; From d8fc6380979d28c48057b0cb84a59cf1ce574922 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 01:48:23 +0900 Subject: [PATCH 251/281] =?UTF-8?q?[BE]=20TEST:=20DTO=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20project=20api=20test=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/project.api.test.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/server/test/project.api.test.js b/server/test/project.api.test.js index 570ec9b4..db18f29d 100644 --- a/server/test/project.api.test.js +++ b/server/test/project.api.test.js @@ -266,16 +266,16 @@ describe('post task', () => { done(); }); - it('project 없이 생성', async done => { - // TODO: validation 체크하는 로직 생각해서 테스트 코드 작성해야 합니다. - // given + // it('project 없이 생성', async done => { + // // TODO: validation 체크하는 로직 생각해서 테스트 코드 작성해야 합니다. + // // given - // when + // // when - // then - expect(true).toBeFalsy(); - done(); - }); + // // then + // expect(true).toBeFalsy(); + // done(); + // }); it('label 없이 생성', async done => { // given @@ -381,7 +381,7 @@ describe('post task', () => { done(); }); - it('유요하지 않은 duedate 생성', async done => { + it('유효하지 않은 duedate 생성', async done => { // given const expectedProjectId = seeder.projects[0].id; const expectedSectionId = seeder.sections[0].id; @@ -404,7 +404,7 @@ describe('post task', () => { // then expect(res.status).toBe(status.BAD_REQUEST.CODE); - expect(res.body.message).toBe('유효하지 않은 dueDate'); + expect(res.body.message).toBe('dueDate는 현재시간보다 이전을 설정할 수 없습니다.'); done(); }); }); From 4cdccc24957110ff0db196c1ec942339c4b60966 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 02:11:19 +0900 Subject: [PATCH 252/281] =?UTF-8?q?[BE]=20CHORE:=20es7=20=EB=AC=B8?= =?UTF-8?q?=EB=B2=95=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20=EB=B0=8F?= =?UTF-8?q?=20eslint=20babel=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/.eslintrc.js | 4 ++++ server/jsconfig.json | 6 ++++++ server/package.json | 1 + 3 files changed, 11 insertions(+) create mode 100644 server/jsconfig.json diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 46a1d525..39580dbc 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -8,8 +8,12 @@ module.exports = { }, plugins: ['prettier', 'jest'], extends: ['airbnb-base', 'eslint-config-prettier', 'prettier'], + parser: '@babel/eslint-parser', parserOptions: { ecmaVersion: 12, + babelOptions: { + configFile: './server/.babelrc.js', + }, }, rules: { 'prettier/prettier': [ diff --git a/server/jsconfig.json b/server/jsconfig.json new file mode 100644 index 00000000..c94d37e3 --- /dev/null +++ b/server/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + }, + "exclude": ["node_modules"] +} diff --git a/server/package.json b/server/package.json index 95307003..ef166951 100644 --- a/server/package.json +++ b/server/package.json @@ -50,6 +50,7 @@ "uuid": "^8.3.1" }, "devDependencies": { + "@babel/eslint-parser": "^7.12.1", "@babel/plugin-proposal-decorators": "^7.12.1", "concurrently": "^5.3.0", "eslint": "^7.13.0", From 727601275686cc39b0d720966b674f7e44c182d6 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 02:31:13 +0900 Subject: [PATCH 253/281] =?UTF-8?q?[BE]=20FIX:=20color=20=EA=B0=92=20allow?= =?UTF-8?q?Null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/models/project.js b/server/src/models/project.js index 8f0f8bac..3e56c26a 100644 --- a/server/src/models/project.js +++ b/server/src/models/project.js @@ -14,7 +14,7 @@ module.exports = sequelize => { type: DataTypes.STRING, }, color: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, isList: { From 67c0155702e1d184060c6fe90f3813db22f31b43 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 02:31:42 +0900 Subject: [PATCH 254/281] =?UTF-8?q?[FE]=20FEAT:=20=EB=82=98=EB=88=94?= =?UTF-8?q?=EB=B0=94=EB=A5=B8=EA=B3=A0=EB=94=95=20=ED=8F=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/App.vue b/client/src/App.vue index 3b093f9c..86cc1085 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -31,3 +31,11 @@ export default { }, }; + + From f8d8ae7b4cf136753d5165654d589e55e1193ff2 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 02:32:27 +0900 Subject: [PATCH 255/281] =?UTF-8?q?[FE]=20CHORE:=20package-lock.json=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 279 +++++++++++++++++++++++++++++++++------ 1 file changed, 241 insertions(+), 38 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index d1a6de46..b38101e1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2124,17 +2124,6 @@ "unique-filename": "^1.1.1" } }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npm.taobao.org/cliui/download/cliui-6.0.0.tgz", @@ -2198,18 +2187,6 @@ "supports-color": "^7.0.0" } }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "optional": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz", @@ -2300,18 +2277,6 @@ "webpack-sources": "^1.4.3" } }, - "vue-loader-v16": { - "version": "npm:vue-loader@16.1.0", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.0.tgz", - "integrity": "sha512-fTtCdI7VeyNK0HP4q4y9Z9ts8TUeaF+2/FjKx8CJ/7/Oem1rCX7zIJe+d+jLrVnVNQjENd3gqmANraLcdRWwnQ==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - } - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-6.2.0.tgz", @@ -2814,7 +2779,6 @@ "version": "1.0.10", "resolved": "https://registry.npm.taobao.org/argparse/download/argparse-1.0.10.tgz", "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -9133,6 +9097,14 @@ "verror": "1.10.0" } }, + "katex": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz", + "integrity": "sha1-EkGOCRIcBckgQbazuftrqyE8tvM=", + "requires": { + "match-at": "^0.1.0" + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz", @@ -9198,6 +9170,14 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, + "linkify-it": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-1.2.4.tgz", + "integrity": "sha1-B3NSbDF8j9E71TTuHRgP+Iq/iBo=", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npm.taobao.org/load-json-file/download/load-json-file-4.0.0.tgz", @@ -9460,6 +9440,99 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-6.1.1.tgz", + "integrity": "sha1-ztA39Ec+6fUVOsQU933IPJG6knw=", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "~1.2.2", + "mdurl": "~1.0.1", + "uc.micro": "^1.0.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + } + } + }, + "markdown-it-abbr": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz", + "integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g=" + }, + "markdown-it-deflist": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz", + "integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==" + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" + }, + "markdown-it-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-2.0.0.tgz", + "integrity": "sha1-FOnE9o/xLPNU+jZa43gnboEEypQ=" + }, + "markdown-it-ins": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-2.0.0.tgz", + "integrity": "sha1-papqMPHi9x6Ul1Z8/f9A8f3mdIM=" + }, + "markdown-it-katex": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz", + "integrity": "sha1-17hqGuoLnWSW+rTnkZoY/e9YnDk=", + "requires": { + "katex": "^0.6.0" + } + }, + "markdown-it-mark": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-2.0.0.tgz", + "integrity": "sha1-RqGqlHEFrtgYiXjgoBYXnkBPQsc=" + }, + "markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g=" + }, + "markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M=" + }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, + "markdown-it-toc-and-anchor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-toc-and-anchor/-/markdown-it-toc-and-anchor-4.2.0.tgz", + "integrity": "sha512-DusSbKtg8CwZ92ztN7bOojDpP4h0+w7BVOPuA3PHDIaabMsERYpwsazLYSP/UlKedoQjOz21mwlai36TQ04EpA==", + "requires": { + "clone": "^2.1.0", + "uslug": "^1.0.4" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + } + } + }, + "match-at": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz", + "integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npm.taobao.org/md5.js/download/md5.js-1.3.5.tgz", @@ -9477,6 +9550,11 @@ "integrity": "sha1-aZs8OKxvHXKAkaZGULZdOIUC/Vs=", "dev": true }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz", @@ -13044,8 +13122,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", @@ -13809,6 +13886,11 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uglify-js": { "version": "3.4.10", "resolved": "https://registry.npm.taobao.org/uglify-js/download/uglify-js-3.4.10.tgz", @@ -13909,6 +13991,11 @@ "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=", "dev": true }, + "unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz", @@ -14033,6 +14120,14 @@ "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=", "dev": true }, + "uslug": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz", + "integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=", + "requires": { + "unorm": ">= 1.0.0" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npm.taobao.org/util/download/util-0.11.1.tgz", @@ -14227,6 +14322,114 @@ } } }, + "vue-loader-v16": { + "version": "npm:vue-loader@16.1.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.1.tgz", + "integrity": "sha512-wz/+HFg/3SBayHWAlZXARcnDTl3VOChrfW9YnxvAweiuyKX/7IGx1ad/4yJHmwhgWlOVYMAbTiI7GV8G33PfGQ==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "optional": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "vue-markdown": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/vue-markdown/-/vue-markdown-2.2.4.tgz", + "integrity": "sha512-hoTX/W1UIdHZrp/b0vpHSsJXAEfWsafaQLgtE2VX4gY8O/C3L2Gabqu95gyG429rL4ML1SwGv+xsPABX7yfFIQ==", + "requires": { + "highlight.js": "^9.12.0", + "markdown-it": "^6.0.1", + "markdown-it-abbr": "^1.0.3", + "markdown-it-deflist": "^2.0.1", + "markdown-it-emoji": "^1.1.1", + "markdown-it-footnote": "^2.0.0", + "markdown-it-ins": "^2.0.0", + "markdown-it-katex": "^2.0.3", + "markdown-it-mark": "^2.0.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-task-lists": "^2.0.1", + "markdown-it-toc-and-anchor": "^4.1.2" + }, + "dependencies": { + "highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==" + } + } + }, "vue-router": { "version": "3.4.9", "resolved": "https://registry.npm.taobao.org/vue-router/download/vue-router-3.4.9.tgz?cache=0&sync_timestamp=1605950629198&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-router%2Fdownload%2Fvue-router-3.4.9.tgz", From 9ff506ec6419505fea6a2ed0a337c6e3ffa53845 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 02:32:52 +0900 Subject: [PATCH 256/281] =?UTF-8?q?[FE]=20REFACTOR:=20AddProjectModal=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/menu/ProjectListContainer.vue | 6 +-- .../components/project/AddProjectModal.vue | 46 +++++++++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/client/src/components/menu/ProjectListContainer.vue b/client/src/components/menu/ProjectListContainer.vue index 2a5c12f0..ef1424d9 100644 --- a/client/src/components/menu/ProjectListContainer.vue +++ b/client/src/components/menu/ProjectListContainer.vue @@ -6,7 +6,7 @@ 프로젝트 - + mdi-plus @@ -101,10 +101,6 @@ export default { }; }, methods: { - openModalEvent(e) { - e.stopPropagation(); - this.AddDialog = true; - }, pushRoute(projectId) { this.$router.push("/project/" + projectId); }, diff --git a/client/src/components/project/AddProjectModal.vue b/client/src/components/project/AddProjectModal.vue index dc0ac75b..7f72ec4a 100644 --- a/client/src/components/project/AddProjectModal.vue +++ b/client/src/components/project/AddProjectModal.vue @@ -14,14 +14,14 @@ 즐겨찾기 @@ -50,7 +50,7 @@ 보기 - + @@ -61,7 +61,14 @@ 취소 - 추가 + + 추가 + @@ -78,21 +85,28 @@ export default { }, data() { return { - title: "", - color: null, + newProject: { + title: "", + color: null, + isFavorite: false, + isList: true, + }, colors, - isFavorite: false, - isList: true, }; }, methods: { ...mapActions(["addProject"]), ...mapMutations(["SET_ERROR_ALERT"]), + clearProject() { + this.newProject = { title: "", color: null, isFavorite: false, isList: true }; + }, sendCloseModalEvent() { + this.clearProject(); this.$emit("handleAddModal"); }, - newProject() { - if (this.title === "관리함") { + addNewProject() { + const defaultProjectTitle = "관리함"; + if (this.newProject.title === defaultProjectTitle) { this.SET_ERROR_ALERT({ data: { message: "해당 제목으로 프로젝트를 생성할 수 없습니다." }, status: 406, @@ -100,19 +114,15 @@ export default { this.title = ""; return; } - if (!this.color) { + if (!this.newProject.color) { this.SET_ERROR_ALERT({ data: { message: "프로젝트 색상을 지정해주세요" }, status: 406, }); return; } - this.addProject({ - title: this.title, - color: this.color, - isFavorite: this.isFavorite, - isList: this.isList, - }); + this.addProject(this.newProject); + this.clearProject(); this.$emit("handleAddModal"); }, }, From dd65a7e96e28eff73af4ab60f119a8ea99f5d3cd Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 02:51:36 +0900 Subject: [PATCH 257/281] =?UTF-8?q?[BE]=20TEST:=20task=20get=20API=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/task.api.test.js | 82 ++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/server/test/task.api.test.js b/server/test/task.api.test.js index e9403cdd..ff5518d6 100644 --- a/server/test/task.api.test.js +++ b/server/test/task.api.test.js @@ -4,6 +4,7 @@ const app = require('@root/app'); const seeder = require('@test/test-seed'); const status = require('@test/response-status'); const { createJWT } = require('@utils/auth'); +const errorMessage = require('@models/dto/error-messages'); beforeAll(async done => { await seeder.up(); @@ -35,16 +36,11 @@ describe('get All task', () => { .set('Authorization', `Bearer ${createJWT(expectedUser)}`); const { tasks } = res.body; + // then expect( - tasks.every(task => - expectedTasks.some( - expectedTask => - Object.entries(expectedTask).toString() === Object.entries(task).toString(), - ), - ), + tasks.every(task => expectedTasks.some(expectedTask => expectedTask.id === task.id)), ).toBeTruthy(); - // expect(tasks).toStrictEqual(expectedTasks); done(); } catch (err) { done(err); @@ -68,10 +64,24 @@ describe('get All task', () => { done(err); } }); + it('토큰 값이 없는 경우 ', async done => { + // given + try { + // when + const res = await request(app).get('/api/task'); + + // then + expect(res.status).toBe(status.UNAUTHORIZED.CODE); + expect(res.body.message).toBe(status.UNAUTHORIZED.MSG); + done(); + } catch (err) { + done(err); + } + }); }); describe('get task by id', () => { - it('get task by id 일반', async done => { + it('get task by id 성공', async done => { // given const taskId = seeder.tasks[0].id; const expectedChildren = seeder.tasks.filter(task => task.parentId === taskId); @@ -91,6 +101,62 @@ describe('get task by id', () => { ).toBeTruthy(); }); + done(); + } catch (err) { + done(err); + } + }); + it('잘못된 id 값 요청', async done => { + // given + const taskId = 'invalidId'; + + try { + // when + const res = await request(app) + .get(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('id')); + done(); + } catch (err) { + done(err); + } + }); + it('자신의 task id가 아닌 경우', async done => { + // given + const taskId = seeder.tasks[0].id; + + try { + // when + const res = await request(app) + .get(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`); + + // then + expect(res.status).toBe(status.FORBIDDEN.CODE); + expect(res.body.message).toBe(status.FORBIDDEN.MSG); + + done(); + } catch (err) { + done(err); + } + }); + it('존재하지 않는 task id인 경우', async done => { + // given + const taskId = 'c213d58a-661a-4395-b0da-fb48dc11fa2e'; + + try { + // when + const res = await request(app) + .get(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); + + // then + expect(res.status).toBe(status.NOT_FOUND.CODE); + expect(res.body.message).toBe(status.NOT_FOUND.MSG); + done(); } catch (err) { done(err); From b839d8d16d1945f31e41f5593ea513d7895c3db8 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 02:53:51 +0900 Subject: [PATCH 258/281] =?UTF-8?q?[BE]=20FEAT:=20task=20dto=EC=97=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=9E=91=EC=97=85=20validation=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/dto/task.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js index f8e3022f..62cbe085 100644 --- a/server/src/models/dto/task.js +++ b/server/src/models/dto/task.js @@ -16,8 +16,8 @@ const { isAfterToday } = require('@utils/validator'); class TaskDto { @IsEmpty({ groups: ['create'], message: errorMessage.UNNECESSARY_INPUT_ERROR('id') }) - @IsString() - @IsUUID('4') + @IsString({ groups: ['retrieve'], message: errorMessage.TYPE_ERROR('id') }) + @IsUUID('4', { groups: ['retrieve'], message: errorMessage.INVALID_INPUT_ERROR('id') }) id; @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('title') }) From 2d1c1f4b775bf9b68aaaff3bd20e8d02587255d7 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 03:00:41 +0900 Subject: [PATCH 259/281] =?UTF-8?q?[BE]=20FEAT:=20=EC=9E=91=EC=97=85=20con?= =?UTF-8?q?troller=EC=97=90=20=EC=83=81=EC=84=B8=20API=20validation=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/task.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index 8015b91f..11e70fff 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -5,7 +5,17 @@ const { asyncTryCatch } = require('@utils/async-try-catch'); const { responseHandler } = require('@utils/handler'); const getTaskById = asyncTryCatch(async (req, res) => { - const task = await taskService.retrieveById(req.params.taskId); + const id = req.params.taskId; + try { + await validator(TaskDto, { id }, { groups: ['retrieve'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + const task = await taskService.retrieveById(id); responseHandler(res, 200, task); }); From 1b6f482c08a83999388c01917931c8db04f218f3 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 03:32:51 +0900 Subject: [PATCH 260/281] =?UTF-8?q?[BE]=20TEST:=20task=20API=20patch=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/task.api.test.js | 160 ++++++++++++++++++++++++++++++----- 1 file changed, 141 insertions(+), 19 deletions(-) diff --git a/server/test/task.api.test.js b/server/test/task.api.test.js index ff5518d6..dc36f55c 100644 --- a/server/test/task.api.test.js +++ b/server/test/task.api.test.js @@ -165,12 +165,13 @@ describe('get task by id', () => { }); describe('patch task with id', () => { - it('patch task with id 일반', async done => { + it('patch task with id 성공', async done => { // given - const newTask = { + const taskId = seeder.tasks[0].id; + const patchTask = { title: '할일', projectId: seeder.projects[0].id, - labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + sessionId: seeder.sessions[0].id, priorityId: seeder.priorities[0].id, dueDate: new Date(), parentId: null, @@ -181,9 +182,9 @@ describe('patch task with id', () => { try { // when const res = await request(app) - .patch(`/api/task/${seeder.tasks[1].id}`) + .patch(`/api/task/${taskId}`) .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); + .send(patchTask); // then expect(res.status).toBe(status.SUCCESS.CODE); @@ -193,26 +194,17 @@ describe('patch task with id', () => { done(err); } }); - - it('patch task without labels', async done => { + it('isDone 성공', async done => { // given - const newTask = { - title: '할일', - projectId: seeder.projects[0].id, - labelIdList: JSON.stringify([]), - priorityId: seeder.priorities[0].id, - dueDate: new Date(), - parentId: null, - alarmId: seeder.alarms[0].id, - position: 1, - }; + const taskId = seeder.tasks[0].id; + const patchTask = { isDone: true }; try { // when const res = await request(app) - .patch(`/api/task/${seeder.tasks[2].id}`) + .patch(`/api/task/${taskId}`) .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) - .send(newTask); + .send(patchTask); // then expect(res.status).toBe(status.SUCCESS.CODE); @@ -222,6 +214,136 @@ describe('patch task with id', () => { done(err); } }); + + it('id값이 포함된 수정', async done => { + // given + const patchTask = { id: seeder.tasks[0].id, title: 'ㅁㄴㅇ' }; + + // when + const res = await request(app) + .patch(`/api/task/${patchTask.id}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('title')); + done(); + }); + it('잘못된 title 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { title: '' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('title')); + done(); + }); + + it('잘못된 parentId 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { parentId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('parentId')); + done(); + }); + + it('잘못된 projectId 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { projectId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('projectId')); + done(); + }); + it('잘못된 priorityId 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { priorityId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('priorityId')); + done(); + }); + it('잘못된 alarmId 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { alarmId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('alarmId')); + done(); + }); + it('잘못된 isDone 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { isDone: 'hi' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.TYPE_ERROR('isDone')); + done(); + }); + it('잘못된 duedate 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { duedate: new Date('2020-11-11') }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.DUEDATE_ERROR); + done(); + }); }); describe('delete task', () => { From 657185f40cad8ae7a52b593c6b264942c00b0ae1 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 03:41:20 +0900 Subject: [PATCH 261/281] =?UTF-8?q?[BE]=20FEAT:=20task=20dto=20patch=20val?= =?UTF-8?q?idation=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/dto/task.js | 54 +++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js index 62cbe085..860b1f31 100644 --- a/server/src/models/dto/task.js +++ b/server/src/models/dto/task.js @@ -1,12 +1,10 @@ const { IsInt, IsString, - IsNotEmpty, IsBoolean, IsArray, MinLength, IsDateString, - ValidateIf, IsUUID, IsOptional, IsEmpty, @@ -15,48 +13,62 @@ const errorMessage = require('@models/dto/error-messages'); const { isAfterToday } = require('@utils/validator'); class TaskDto { - @IsEmpty({ groups: ['create'], message: errorMessage.UNNECESSARY_INPUT_ERROR('id') }) + @IsEmpty({ groups: ['create', 'patch'], message: errorMessage.UNNECESSARY_INPUT_ERROR('id') }) @IsString({ groups: ['retrieve'], message: errorMessage.TYPE_ERROR('id') }) @IsUUID('4', { groups: ['retrieve'], message: errorMessage.INVALID_INPUT_ERROR('id') }) id; - @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('title') }) - @MinLength(1, { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('title') }) + @IsOptional({ groups: ['patch'] }) + @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('title') }) + @MinLength(1, { groups: ['create', 'patch'], message: errorMessage.INVALID_INPUT_ERROR('title') }) title; + @IsOptional({ groups: ['patch'] }) @IsDateString( { strict: true }, - { groups: ['create'], message: errorMessage.TYPE_ERROR('dueDate') }, + { groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('dueDate') }, ) - @isAfterToday('dueDate', { groups: ['create'], message: errorMessage.DUEDATE_ERROR }) + @isAfterToday('dueDate', { groups: ['create', 'patch'], message: errorMessage.DUEDATE_ERROR }) dueDate; - @IsInt() - @IsNotEmpty() + @IsOptional({ groups: ['patch'] }) + @IsInt({ groups: ['patch'], message: errorMessage.TYPE_ERROR('position') }) position; - @IsBoolean() - @IsNotEmpty() + @IsOptional({ groups: ['patch'] }) + @IsBoolean({ groups: ['patch'], message: errorMessage.TYPE_ERROR('position') }) isDone; - @IsOptional({ groups: ['create'] }) - @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('sectionId') }) - @IsUUID('4', { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('parentId') }) + @IsOptional({ groups: ['create', 'patch'] }) + @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('sectionId') }) + @IsUUID('4', { + groups: ['create', 'patch'], + message: errorMessage.INVALID_INPUT_ERROR('parentId'), + }) parentId; - @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('sectionId') }) - @IsUUID('4', { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('sectionId') }) + @IsOptional({ groups: ['patch'] }) + @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('sectionId') }) + @IsUUID('4', { + groups: ['create', 'patch'], + message: errorMessage.INVALID_INPUT_ERROR('sectionId'), + }) sectionId; - @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('projectId') }) - @IsUUID('4', { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('projectId') }) + @IsOptional({ groups: ['patch'] }) + @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('projectId') }) + @IsUUID('4', { + groups: ['create', 'patch'], + message: errorMessage.INVALID_INPUT_ERROR('projectId'), + }) projectId; - @IsString() - @IsUUID('4') + @IsOptional({ groups: ['patch'] }) + @IsUUID('4', { groups: ['patch'], message: errorMessage.INVALID_INPUT_ERROR('priorityId') }) priorityId; - @IsUUID('4') + @IsOptional({ groups: ['patch'] }) + @IsUUID('4', { groups: ['patch'], message: errorMessage.INVALID_INPUT_ERROR('alarmId') }) alarmId; @IsArray From 81b46967161e91049c9208b4582a287c04ec5092 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 03:45:56 +0900 Subject: [PATCH 262/281] =?UTF-8?q?[BE]=20FEAT:=20project=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(=EA=B0=80=EC=9E=A5=20=EB=82=98=EC=A4=91?= =?UTF-8?q?=EC=97=90=20=EC=83=9D=EC=84=B1=EB=90=9C=20sectionId=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/services/project.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/server/src/services/project.js b/server/src/services/project.js index 21f2f2c2..0cf4b050 100644 --- a/server/src/services/project.js +++ b/server/src/services/project.js @@ -14,11 +14,15 @@ const retrieveProjects = async () => { 'isFavorite', 'isList', [sequelize.fn('COUNT', sequelize.col('tasks.id')), 'taskCount'], + [sequelize.fn('max', sequelize.col('sections.id')), 'sectionId'], + ], + include: [ + { + model: models.task, + attributes: [], + }, + { model: models.section, attributes: [], order: [['createdAt', 'ASC']] }, ], - include: { - model: models.task, - attributes: [], - }, group: ['project.id'], }); From 9033c33211d14afa8340bb594655bc3a1d467468 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 03:49:56 +0900 Subject: [PATCH 263/281] =?UTF-8?q?[BE]=20FEAT:=20task=20controller?= =?UTF-8?q?=EC=97=90=20patch=20validation=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/task.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index 11e70fff..adb81513 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -17,7 +17,7 @@ const getTaskById = asyncTryCatch(async (req, res) => { const task = await taskService.retrieveById(id); - responseHandler(res, 200, task); + responseHandler(res, 200, { task }); }); const getAllTasks = asyncTryCatch(async (req, res) => { @@ -39,22 +39,24 @@ const createTask = asyncTryCatch(async (req, res) => { throw err; } - await taskService.create({ projectId, sectionId, ...req.body }); + await taskService.create(task); responseHandler(res, 201, { message: 'ok' }); }); const updateTask = asyncTryCatch(async (req, res) => { - const { dueDate } = req.body; + const { taskId } = req.params; + const task = { ...req.body }; - if (!isValidDueDate(dueDate)) { - const err = new Error('유효하지 않은 dueDate'); + try { + await validator(TaskDto, task, { groups: ['patch'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); err.status = 400; throw err; } - const { taskId } = req.params; - - await taskService.update({ id: taskId, ...req.body }); + await taskService.update({ id: taskId, ...task }); responseHandler(res, 200, { message: 'ok' }); }); From 4be5f4f91d1dc79e08cd478168e6417f413043df Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 04:00:36 +0900 Subject: [PATCH 264/281] =?UTF-8?q?[BE]=20TEST:=20task=20patch=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test/task.api.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/test/task.api.test.js b/server/test/task.api.test.js index dc36f55c..495eeda6 100644 --- a/server/test/task.api.test.js +++ b/server/test/task.api.test.js @@ -92,7 +92,9 @@ describe('get task by id', () => { .get(`/api/task/${taskId}`) .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); - const recievedChildren = res.body.tasks.filter(task => task.parentId === taskId); + const recievedChildren = res.body.task.tasks.filter( + childTask => childTask.parentId === taskId, + ); // then recievedChildren.forEach(recievedChild => { @@ -171,7 +173,7 @@ describe('patch task with id', () => { const patchTask = { title: '할일', projectId: seeder.projects[0].id, - sessionId: seeder.sessions[0].id, + sectionId: seeder.sections[0].id, priorityId: seeder.priorities[0].id, dueDate: new Date(), parentId: null, @@ -217,7 +219,7 @@ describe('patch task with id', () => { it('id값이 포함된 수정', async done => { // given - const patchTask = { id: seeder.tasks[0].id, title: 'ㅁㄴㅇ' }; + const patchTask = { id: seeder.tasks[0].id, title: '졸리다' }; // when const res = await request(app) @@ -227,7 +229,7 @@ describe('patch task with id', () => { // then expect(res.status).toBe(status.BAD_REQUEST.CODE); - expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('title')); + expect(res.body.message).toBe(errorMessage.UNNECESSARY_INPUT_ERROR('id')); done(); }); it('잘못된 title 수정', async done => { From 5e3f9d76011a2e298eed624fb52054c46df798b3 Mon Sep 17 00:00:00 2001 From: "dimpl@JinYoung" Date: Wed, 9 Dec 2020 04:01:45 +0900 Subject: [PATCH 265/281] =?UTF-8?q?[BE]=20STYLE:=20dto=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/models/dto/task.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js index 860b1f31..10c6fd45 100644 --- a/server/src/models/dto/task.js +++ b/server/src/models/dto/task.js @@ -36,11 +36,11 @@ class TaskDto { position; @IsOptional({ groups: ['patch'] }) - @IsBoolean({ groups: ['patch'], message: errorMessage.TYPE_ERROR('position') }) + @IsBoolean({ groups: ['patch'], message: errorMessage.TYPE_ERROR('isDone') }) isDone; @IsOptional({ groups: ['create', 'patch'] }) - @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('sectionId') }) + @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('parentId') }) @IsUUID('4', { groups: ['create', 'patch'], message: errorMessage.INVALID_INPUT_ERROR('parentId'), From 36c1915fd8ffba2fb9ba6705250526c5dbe24050 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 04:48:47 +0900 Subject: [PATCH 266/281] =?UTF-8?q?[FE]=20FEAT:=20loading=20spinner=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bus, mixin 사용 --- client/src/App.vue | 32 ++++++++++++++++--- client/src/components/common/Spinner.vue | 27 ++++++++++++++++ .../components/today/TodayTasksContainer.vue | 12 ++++--- client/src/mixins/ListMixins.js | 7 ++++ client/src/router/index.js | 3 ++ client/src/utils/bus.js | 2 ++ client/src/views/Home.vue | 2 ++ client/src/views/Login.vue | 2 ++ client/src/views/Project.vue | 2 ++ client/src/views/Today.vue | 2 ++ 10 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 client/src/components/common/Spinner.vue create mode 100644 client/src/mixins/ListMixins.js create mode 100644 client/src/utils/bus.js diff --git a/client/src/App.vue b/client/src/App.vue index 86cc1085..3aa2f454 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,18 +1,38 @@ diff --git a/client/src/components/common/Spinner.vue b/client/src/components/common/Spinner.vue new file mode 100644 index 00000000..8f4cf802 --- /dev/null +++ b/client/src/components/common/Spinner.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/client/src/components/today/TodayTasksContainer.vue b/client/src/components/today/TodayTasksContainer.vue index bc4c6946..b8e366b7 100644 --- a/client/src/components/today/TodayTasksContainer.vue +++ b/client/src/components/today/TodayTasksContainer.vue @@ -12,7 +12,7 @@ - + @@ -27,7 +27,7 @@ - Open Dialog + @@ -35,8 +35,9 @@ diff --git a/client/src/mixins/ListMixins.js b/client/src/mixins/ListMixins.js new file mode 100644 index 00000000..ab331177 --- /dev/null +++ b/client/src/mixins/ListMixins.js @@ -0,0 +1,7 @@ +import bus from "@/utils/bus.js"; + +export default { + mounted() { + bus.$emit("end:spinner"); + }, +}; diff --git a/client/src/router/index.js b/client/src/router/index.js index 3a4028c3..8f83538b 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -6,10 +6,12 @@ import Project from "@/views/Project.vue"; import Home from "@/views/Home.vue"; import Task from "@/views/Task"; import userAPI from "@/api/user"; +import bus from "@/utils/bus.js"; Vue.use(VueRouter); const requireAuth = () => (from, to, next) => { + bus.$emit("start:spinner"); if (localStorage.getItem("token")) { return next(); } @@ -17,6 +19,7 @@ const requireAuth = () => (from, to, next) => { }; const redirectHome = () => async (from, to, next) => { + bus.$emit("start:spinner"); try { await userAPI.authorize(); return next("/"); diff --git a/client/src/utils/bus.js b/client/src/utils/bus.js new file mode 100644 index 00000000..968c96ba --- /dev/null +++ b/client/src/utils/bus.js @@ -0,0 +1,2 @@ +import Vue from "vue"; +export default new Vue(); diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 14dd2831..db1a260b 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -15,6 +15,7 @@ diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index f2227ef2..a19caaa0 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -14,6 +14,7 @@ diff --git a/client/src/views/Project.vue b/client/src/views/Project.vue index 729c4b2b..9dcf308b 100644 --- a/client/src/views/Project.vue +++ b/client/src/views/Project.vue @@ -9,6 +9,7 @@ import { mapActions, mapGetters } from "vuex"; import ProjectContainer from "../components/project/ProjectContainer"; import Alert from "@/components/common/Alert"; +import ListMixin from "@/mixins/ListMixins.js"; export default { components: { ProjectContainer, Alert }, @@ -26,5 +27,6 @@ export default { created() { this.fetchCurrentProject(this.$route.params.projectId); }, + mixins: [ListMixin], }; diff --git a/client/src/views/Today.vue b/client/src/views/Today.vue index 9579b376..3244d58c 100644 --- a/client/src/views/Today.vue +++ b/client/src/views/Today.vue @@ -5,6 +5,7 @@ From f7d2a9180c7390410a1af3607e5d77adc55b8d20 Mon Sep 17 00:00:00 2001 From: Jin Young Park Date: Wed, 9 Dec 2020 04:52:13 +0900 Subject: [PATCH 267/281] =?UTF-8?q?[FE]=20CHORE:=20ProjectList=20todo=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/menu/FavoriteProjectList.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue index a96f1ba6..78fe2897 100644 --- a/client/src/components/menu/FavoriteProjectList.vue +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -25,7 +25,7 @@ v-for="favoriteProjectInfo in favoriteProjectInfos" class="pl-4" :key="favoriteProjectInfo.id" - @click="pushRoute(favoriteProjectInfo.id)" + :to="`/project/${favoriteProjectInfo.id}`" > mdi-circle @@ -44,6 +44,8 @@ + + 프로젝트 수정 @@ -87,9 +89,6 @@ export default { return { updateDialog: false, deleteDialog: false, projectId: "" }; }, methods: { - pushRoute(projectId) { - this.$router.push("/project/" + projectId); - }, openUpdateDialog(projectId) { this.projectId = projectId; this.updateDialog = true; From 3b376767f562c7b5c79ed5232e2b0162437da209 Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 9 Dec 2020 13:42:25 +0900 Subject: [PATCH 268/281] =?UTF-8?q?[BE]=20REFACTOR:=20section=20dto=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B0=8F=20section=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/controllers/section.js | 29 +++++++++++++++++++++++++++-- server/src/models/dto/section.js | 16 ++++++++++++++++ server/src/models/dto/task.js | 27 --------------------------- 3 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 server/src/models/dto/section.js diff --git a/server/src/controllers/section.js b/server/src/controllers/section.js index ce36c346..6e3abc78 100644 --- a/server/src/controllers/section.js +++ b/server/src/controllers/section.js @@ -1,9 +1,22 @@ +const SectionDto = require('@models/dto/section'); const sectionService = require('@services/section'); +const { validator, getErrorMsg } = require('@utils/validator'); const { responseHandler } = require('@utils/handler'); const { asyncTryCatch } = require('@utils/async-try-catch'); const createSection = asyncTryCatch(async (req, res) => { - await sectionService.create({ projectId: req.params.projectId, ...req.body }); + const { projectId } = req.params; + try { + await validator(SectionDto, req.body, { groups: ['create'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + // TODO projectId를 따로 빼야 하나 ? + await sectionService.create({ projectId, ...req.body }); responseHandler(res, 201, { message: 'ok' }); }); @@ -15,7 +28,19 @@ const updateTaskPositions = asyncTryCatch(async (req, res) => { }); const updateSection = asyncTryCatch(async (req, res) => { - await sectionService.update({ id: req.params.sectionId, ...req.body }); + const { sectionId } = req.params; + + try { + await validator(SectionDto, req.body, { groups: ['update'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + // TODO sectionId를 따로 빼야 하나 ? + await sectionService.update({ id: sectionId, ...req.body }); responseHandler(res, 200, { message: 'ok' }); }); diff --git a/server/src/models/dto/section.js b/server/src/models/dto/section.js new file mode 100644 index 00000000..4fdc5866 --- /dev/null +++ b/server/src/models/dto/section.js @@ -0,0 +1,16 @@ +const { IsUUID, IsString, IsInt, MinLength } = require('class-validator'); +const errorMessage = require('@models/dto/error-messages'); + +class SectionDto { + @IsUUID('4') + id; + + @IsString({ groups: ['create', 'update'] }, { message: errorMessage.wrongProperty('title') }) + @MinLength(1, { groups: ['create', 'update'] }, { message: errorMessage.wrongProperty('title') }) + title; + + @IsInt() + position; +} + +module.exports = SectionDto; diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js index 1fade6e3..95a6686d 100644 --- a/server/src/models/dto/task.js +++ b/server/src/models/dto/task.js @@ -68,33 +68,6 @@ class TaskDto { @IsArray orderedTasks; - - // constructor({ id, title, dueDate, position, isDone }) { - // this.id = id; - // this.title = title; - // this.dueDate = dueDate; - // this.position = position; - // this.isDone = isDone; - // } } -// const task = new TaskDto(); -// task.id = 'ff4dd832-1567-4d74-b41d-bd85e96ce329'; -// task.title = 'zkzkzk'; -// task.dueDate = new Date('2020-12-07'); -// task.position = 4; -// task.isDone = true; -// task.parentId = 'ff4dd832-1567-4d74-b41d-bd85e96ce329'; - -// await validateOrReject(task, { gropus }).catch(errors => { -// console.log('Promise rejected (validation failed). Errors: ', errors); -// }); - -// sectionId; - -// projectId -// priorityId; -// alarmId; -// orderedTasks - module.exports = TaskDto; From 644e3f607cd1f56a8f271ac13dd96f0a93b49a1f Mon Sep 17 00:00:00 2001 From: shkilo Date: Wed, 9 Dec 2020 13:43:23 +0900 Subject: [PATCH 269/281] =?UTF-8?q?[FE]=20REFACTOR:=20whale=20api=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/project/AddTask.vue | 7 ++++--- client/src/utils/whaleApi/index.js | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 client/src/utils/whaleApi/index.js diff --git a/client/src/components/project/AddTask.vue b/client/src/components/project/AddTask.vue index 8bb8865d..f0b70b93 100644 --- a/client/src/components/project/AddTask.vue +++ b/client/src/components/project/AddTask.vue @@ -61,6 +61,7 @@