├── BE
├── .prettierrc
├── src
│ ├── interfaces
│ │ ├── response.ts
│ │ ├── tag.ts
│ │ ├── assignee.ts
│ │ ├── event.ts
│ │ ├── label.ts
│ │ ├── user.ts
│ │ ├── milestone.ts
│ │ ├── comment.ts
│ │ └── issue.ts
│ ├── utils
│ │ ├── magicnumber.ts
│ │ ├── response.ts
│ │ └── filter.ts
│ ├── index.ts
│ ├── routes
│ │ ├── event.ts
│ │ ├── tag.ts
│ │ ├── assignee.ts
│ │ ├── index.ts
│ │ ├── label.ts
│ │ ├── comment.ts
│ │ ├── milestone.ts
│ │ ├── issue.ts
│ │ └── auth.ts
│ ├── providers
│ │ ├── database.ts
│ │ └── passport.ts
│ ├── controllers
│ │ ├── event.ts
│ │ ├── user.ts
│ │ ├── label.ts
│ │ ├── comment.ts
│ │ ├── tag.ts
│ │ ├── assignee.ts
│ │ └── milestone.ts
│ ├── models
│ │ ├── event.ts
│ │ ├── user.ts
│ │ ├── tag.ts
│ │ ├── model.ts
│ │ ├── assignee.ts
│ │ ├── label.ts
│ │ ├── comment.ts
│ │ └── milestone.ts
│ └── app.ts
├── depoly.sh
├── README.md
├── .eslintrc.json
└── package.json
├── FE
├── .prettierrc
├── public
│ └── index.html
├── .babelrc
├── src
│ ├── index.js
│ ├── pages
│ │ ├── registerPage.jsx
│ │ ├── issueCreatePage.jsx
│ │ ├── issueDetailPage.jsx
│ │ ├── milestoneEditPage.jsx
│ │ ├── milestoneCreatePage.jsx
│ │ ├── callback.jsx
│ │ ├── loginPage.jsx
│ │ ├── milestoneListPage.jsx
│ │ ├── labelListPage.jsx
│ │ └── issueListPage.jsx
│ ├── util
│ │ ├── axiosApi.js
│ │ └── useRequest.jsx
│ ├── component
│ │ ├── labelListPage
│ │ │ ├── filter
│ │ │ │ └── labelFilter.jsx
│ │ │ ├── buttons
│ │ │ │ ├── moveToCreateLabelButton.jsx
│ │ │ │ ├── moveToLabelButton.jsx
│ │ │ │ └── moveToMilestoneButton.jsx
│ │ │ └── element
│ │ │ │ ├── labelList.jsx
│ │ │ │ └── label.jsx
│ │ ├── header
│ │ │ └── header.jsx
│ │ ├── milestonePage
│ │ │ ├── progressbar.jsx
│ │ │ ├── buttons
│ │ │ │ ├── editButton.jsx
│ │ │ │ ├── openFilterBtn.jsx
│ │ │ │ ├── closeFilterBtn.jsx
│ │ │ │ ├── newButton.jsx
│ │ │ │ ├── closeButton.jsx
│ │ │ │ └── deleteButton.jsx
│ │ │ ├── information.jsx
│ │ │ ├── list
│ │ │ │ ├── milestoneListHeader.jsx
│ │ │ │ └── milestoneList.jsx
│ │ │ ├── buttonWrapper.jsx
│ │ │ └── milestoneState.jsx
│ │ ├── issueListPage
│ │ │ ├── buttons
│ │ │ │ ├── moveToCreateIssueButton.jsx
│ │ │ │ ├── moveToMilestoneButton.jsx
│ │ │ │ └── moveToLabelButton.jsx
│ │ │ └── element
│ │ │ │ ├── issuelist.jsx
│ │ │ │ ├── issue.jsx
│ │ │ │ ├── userDropdown.jsx
│ │ │ │ ├── labelDropdown.jsx
│ │ │ │ ├── milestoneDropdown.jsx
│ │ │ │ ├── checkFilter.jsx
│ │ │ │ └── issueFilter.jsx
│ │ └── loginPage
│ │ │ ├── buttons
│ │ │ ├── loginGithubButton.jsx
│ │ │ └── loginSubmitButton.jsx
│ │ │ └── form
│ │ │ └── loginform.jsx
│ └── app.js
├── .eslintrc.json
├── README.md
├── webpack.config.js
└── package.json
├── iOS
├── Image
│ ├── diagram.jpg
│ ├── 이미지.001.jpeg
│ ├── 이미지.002.jpeg
│ ├── 이미지.003.jpeg
│ └── 이미지.004.jpeg
└── IssueTracker
│ ├── IssueTracker
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── 16.png
│ │ │ ├── 20.png
│ │ │ ├── 29.png
│ │ │ ├── 32.png
│ │ │ ├── 40.png
│ │ │ ├── 48.png
│ │ │ ├── 50.png
│ │ │ ├── 55.png
│ │ │ ├── 57.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 64.png
│ │ │ ├── 72.png
│ │ │ ├── 76.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ ├── 88.png
│ │ │ ├── 100.png
│ │ │ ├── 1024.png
│ │ │ ├── 114.png
│ │ │ ├── 120.png
│ │ │ ├── 128.png
│ │ │ ├── 144.png
│ │ │ ├── 152.png
│ │ │ ├── 167.png
│ │ │ ├── 172.png
│ │ │ ├── 180.png
│ │ │ ├── 196.png
│ │ │ ├── 216.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ ├── apple-camera.imageset
│ │ │ ├── apple-camera.png
│ │ │ ├── apple-camera@2x.png
│ │ │ ├── apple-camera@3x.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Network
│ │ ├── APIServer.swift
│ │ ├── APIConfiguration.swift
│ │ ├── ContentType.swift
│ │ ├── HTTPMethod.swift
│ │ ├── HTTPHeader.swift
│ │ └── NetworkError.swift
│ ├── Entity
│ │ ├── IssueComment.swift
│ │ ├── User.swift
│ │ ├── UserInfo.swift
│ │ ├── AllGetUser.swift
│ │ ├── Milestone.swift
│ │ ├── Label.swift
│ │ ├── Assignee.swift
│ │ └── Comment.swift
│ ├── SignIn
│ │ ├── Models
│ │ │ ├── AppleModel.swift
│ │ │ ├── RequestLogin.swift
│ │ │ ├── AppleUser.swift
│ │ │ └── SignInEndPoint.swift
│ │ └── OAuthManager.swift
│ ├── IssueTracker.entitlements
│ ├── Common
│ │ ├── Extension
│ │ │ ├── Date+timeAgoDisplay.swift
│ │ │ ├── String+asURL.swift
│ │ │ ├── URL+searchToken.swift
│ │ │ ├── Encodable+encoded.swift
│ │ │ ├── String+toDate.swift
│ │ │ ├── UIColor+random.swift
│ │ │ ├── DateFormatter+format.swift
│ │ │ ├── UIColor+visibleTextColor.swift
│ │ │ ├── UIView+shake.swift
│ │ │ ├── Data+decoded.swift
│ │ │ ├── UIVIew+snapshot.swift
│ │ │ ├── UIColor+hexString.swift
│ │ │ ├── UIViewController+hideKeyboardWhenTapped.swift
│ │ │ ├── UIView+IBInspectable.swift
│ │ │ └── UIColor+hex.swift
│ │ ├── PropertyWrapper
│ │ │ ├── KeyChain.swift
│ │ │ └── UserDefault.swift
│ │ └── Views
│ │ │ └── CustomButtonView.swift
│ ├── Label
│ │ ├── LabelPresenter.swift
│ │ ├── Views
│ │ │ └── LabelCollectionViewCell.swift
│ │ ├── Models
│ │ │ ├── LabelViewModel.swift
│ │ │ └── LabelEndPoint.swift
│ │ └── LabelInteractor.swift
│ ├── IssueDetail
│ │ ├── IssueDetailPresenter.swift
│ │ ├── Views
│ │ │ ├── IssueDetailCollectionViewCell.swift
│ │ │ └── IssueDetailCollectionReusableView.swift
│ │ ├── Models
│ │ │ ├── IssueDetailEndPoint.swift
│ │ │ └── IssueDetailViewModel.swift
│ │ ├── IssueDetailBottomSheetViewController.swift
│ │ ├── Edit
│ │ │ └── EditTableViewController.swift
│ │ ├── IssueDetailInteractor.swift
│ │ └── IssueCommentViewController.swift
│ ├── IssueList
│ │ ├── Models
│ │ │ ├── IssueListModelController.swift
│ │ │ └── IssueListViewModel.swift
│ │ ├── IssueListPresenter.swift
│ │ ├── IssueListEndPoint.swift
│ │ ├── Views
│ │ │ └── IssueListCollectionViewCell.swift
│ │ └── IssueListInteractor.swift
│ ├── Edit
│ │ ├── Models
│ │ │ ├── IssueDetailEditEndPoint.swift
│ │ │ └── IssueDetailEditViewModel.swift
│ │ └── Views
│ │ │ └── EditTableViewCell.swift
│ ├── Milestone
│ │ ├── MilestonePresenter.swift
│ │ ├── Models
│ │ │ ├── MilestoneViewModel.swift
│ │ │ ├── MilestoneEndPoint.swift
│ │ │ └── MilestoneCalculator.swift
│ │ ├── MilestoneInterator.swift
│ │ └── Views
│ │ │ └── MilestoneCollectionViewCell.swift
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ ├── Setting
│ │ └── SettingViewController.swift
│ ├── IssueFilter
│ │ ├── Views
│ │ │ └── HeaderView.swift
│ │ └── Models
│ │ │ └── IssueFilterViewModel.swift
│ ├── CreateIssue
│ │ └── Models
│ │ │ └── CreateIssueEndPoint.swift
│ └── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── IssueTracker.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ ├── xcuserdata
│ │ └── jaehyun.xcuserdatad
│ │ │ └── xcschemes
│ │ │ └── xcschememanagement.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── IssueTrackerTests.xcscheme
│ ├── IssueTracker.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── swiftpm
│ │ └── Package.resolved
│ ├── Podfile
│ ├── Podfile.lock
│ ├── .swiftlint.yml
│ └── IssueTrackerTests
│ ├── SignInTests
│ └── SignInEndPointTest.swift
│ ├── IssueTrackerTests.swift
│ ├── Info.plist
│ ├── IssueListTests
│ ├── IssueListFilterTests.swift
│ └── IssueListMakeTests.swift
│ └── IssueList
│ ├── IssueListViewModelTest.swift
│ ├── Interactor.swift
│ └── IssueListEndPointTest.swift
├── .github
├── labeler.yml
├── reviewer-lottery.yml
└── workflows
│ ├── labeler.yml
│ ├── reviewer_lottery.yml
│ └── node.js_CI.yml
└── LICENSE
/BE/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 150,
3 | "tabWidth": 2,
4 | "trailingComma": "es5"
5 | }
--------------------------------------------------------------------------------
/FE/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 150,
3 | "tabWidth": 2,
4 | "trailingComma": "es5"
5 | }
--------------------------------------------------------------------------------
/iOS/Image/diagram.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/Image/diagram.jpg
--------------------------------------------------------------------------------
/iOS/Image/이미지.001.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/Image/이미지.001.jpeg
--------------------------------------------------------------------------------
/iOS/Image/이미지.002.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/Image/이미지.002.jpeg
--------------------------------------------------------------------------------
/iOS/Image/이미지.003.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/Image/이미지.003.jpeg
--------------------------------------------------------------------------------
/iOS/Image/이미지.004.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/Image/이미지.004.jpeg
--------------------------------------------------------------------------------
/BE/src/interfaces/response.ts:
--------------------------------------------------------------------------------
1 | export interface resMessage {
2 | message: string;
3 | httpcode: number;
4 | }
5 |
--------------------------------------------------------------------------------
/BE/depoly.sh:
--------------------------------------------------------------------------------
1 | git stash
2 | git checkout master
3 | git pull
4 | npm run build
5 | pm2 stop npm
6 | pm2 delete npm
7 | pm2 start npm -- start
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/BE/src/utils/magicnumber.ts:
--------------------------------------------------------------------------------
1 | const HTTPCODE = {
2 | SUCCESS: 200,
3 | FAIL: 400,
4 | SERVER_ERR: 500,
5 | };
6 |
7 | export default HTTPCODE;
8 |
--------------------------------------------------------------------------------
/BE/src/interfaces/tag.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface Tag {
3 | id: number | null;
4 | issue_id: number;
5 | label_id: number;
6 | }
7 |
--------------------------------------------------------------------------------
/BE/src/interfaces/assignee.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface Assignee {
3 | id: number | null;
4 | issue_id: number;
5 | user_id: number;
6 | }
7 |
--------------------------------------------------------------------------------
/BE/src/index.ts:
--------------------------------------------------------------------------------
1 | import "module-alias/register";
2 | import App from "./app";
3 |
4 | (async function main() {
5 | const app = new App();
6 | await app.listen();
7 | })();
8 |
--------------------------------------------------------------------------------
/.github/labeler.yml:
--------------------------------------------------------------------------------
1 | 📱APP :
2 | - 'iOS/**'
3 |
4 | 🖥BE :
5 | - 'BE/**'
6 |
7 | 📻FE :
8 | - 'FE/**'
9 |
10 | 🌼WEEK 3 :
11 | - 'FE/**'
12 | - 'BE/**'
13 | - 'iOS/**'
14 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/48.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/55.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/55.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/64.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/88.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/88.png
--------------------------------------------------------------------------------
/BE/src/interfaces/event.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface Event {
3 | id: number | null;
4 | issue_id: number;
5 | user_id: number;
6 | log: string;
7 | created_at: Date;
8 | }
9 |
--------------------------------------------------------------------------------
/BE/src/interfaces/label.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface Label {
3 | id: number | null;
4 | name: string;
5 | description: string;
6 | color: string;
7 | created_at: Date;
8 | }
9 |
--------------------------------------------------------------------------------
/BE/src/interfaces/user.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface User {
3 | id: number | null;
4 | login_id: string;
5 | password: string;
6 | img: string;
7 | created_at: Date;
8 | }
9 |
--------------------------------------------------------------------------------
/FE/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Webpack-for-react
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/172.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/172.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/196.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/216.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/216.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/.github/reviewer-lottery.yml:
--------------------------------------------------------------------------------
1 | groups:
2 | - name: devs
3 | reviewers: 3
4 | usernames:
5 | - gitdog01
6 | - hi0826
7 | - ji3427
8 | - Minkwan-Song
9 | - wogus3602
10 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/apple-camera.imageset/apple-camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/apple-camera.imageset/apple-camera.png
--------------------------------------------------------------------------------
/FE/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets" : [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "babel-plugin-styled-components",
8 | "react-hot-loader/babel"
9 | ]
10 | }
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/apple-camera.imageset/apple-camera@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/apple-camera.imageset/apple-camera@2x.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/apple-camera.imageset/apple-camera@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/IssueTracker-3/HEAD/iOS/IssueTracker/IssueTracker/Assets.xcassets/apple-camera.imageset/apple-camera@3x.png
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/BE/src/interfaces/milestone.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface Milestone {
3 | id: number | null;
4 | name: string;
5 | description: string;
6 | state: boolean;
7 | due_date: Date | null;
8 | created_at: Date;
9 | }
10 |
--------------------------------------------------------------------------------
/BE/src/interfaces/comment.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface Comment {
3 | id: number | null;
4 | issue_id: number;
5 | user_id: number;
6 | body: string;
7 | emoji: string | null;
8 | created_at: Date;
9 | }
10 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/BE/src/routes/event.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import controller from "@controllers/event";
3 |
4 | const router = express.Router();
5 |
6 | router.get("/:issueid", controller.get);
7 | router.post("/:issueid", controller.add);
8 | export = router;
9 |
--------------------------------------------------------------------------------
/BE/src/routes/tag.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import TagController from "../controllers/tag";
3 |
4 | const router = express.Router();
5 | router.get("/:issueid", TagController.get);
6 | router.patch("/:issueid", TagController.edit);
7 | export = router;
8 |
--------------------------------------------------------------------------------
/BE/src/utils/response.ts:
--------------------------------------------------------------------------------
1 | import { resMessage } from "@interfaces/response";
2 |
3 | const makeResponse = (statusCode: number, message: string): resMessage => {
4 | return {
5 | message,
6 | httpcode: statusCode,
7 | };
8 | };
9 |
10 | export default makeResponse;
11 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Network/APIServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIServer.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | struct APIServer {
11 | static let baseURL = "http://101.101.210.34:3000"
12 | }
13 |
--------------------------------------------------------------------------------
/BE/src/routes/assignee.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import assigneeController from "@controllers/assignee";
3 |
4 | const router = express.Router();
5 |
6 | router.get("/:issueid", assigneeController.get);
7 | router.patch("/:issueid", assigneeController.edit);
8 | export = router;
9 |
--------------------------------------------------------------------------------
/BE/src/interfaces/issue.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export interface Issue {
3 | id: number | null;
4 | title: string;
5 | body: string;
6 | user_id: number;
7 | state: boolean;
8 | milestone_id: number | null;
9 | created_at: Date;
10 | closed_at: Date | null;
11 | }
12 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/IssueComment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueComment.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | struct IssueComment: Codable {
11 | let comments: CommentList
12 | let counts: Int
13 | }
14 |
--------------------------------------------------------------------------------
/FE/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDom from "react-dom";
3 | import Header from "@component/header/header";
4 | import App from "./app";
5 |
6 | ReactDom.render(
7 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/SignIn/Models/AppleModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // appleModel.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | struct AppleLogin: Codable {
11 | let authorizationCode: String
12 | let identityToken: String
13 | }
14 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/Podfile:
--------------------------------------------------------------------------------
1 | target 'IssueTracker' do
2 |
3 | pod 'SwiftLint'
4 |
5 | end
6 |
7 | post_install do |installer|
8 | installer.pods_project.targets.each do |target|
9 | target.build_configurations.each do |config|
10 | config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/BE/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | import authController from "@controllers/auth";
3 | const router = express.Router();
4 |
5 | router.get("/", authController.authCheck, (req: Request, res: Response, next: NextFunction) => {
6 | res.send("world");
7 | });
8 |
9 | export = router;
10 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - SwiftLint (0.40.1)
3 |
4 | DEPENDENCIES:
5 | - SwiftLint
6 |
7 | SPEC REPOS:
8 | trunk:
9 | - SwiftLint
10 |
11 | SPEC CHECKSUMS:
12 | SwiftLint: bbfed21bf4451dcbd0f5cdbee44a18e06cf91b12
13 |
14 | PODFILE CHECKSUM: 05c331f76f2dc6cabcdd0a54373b97ce81aa2446
15 |
16 | COCOAPODS: 1.8.4
17 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Network/APIConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIConfiguration.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/10/28.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol APIConfiguration {
11 | var method: HTTPMethod { get }
12 | var path: String { get }
13 | var body: Data? { get }
14 | }
15 |
--------------------------------------------------------------------------------
/BE/src/routes/label.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import LabelController from "@controllers/label";
3 |
4 | const router = express.Router();
5 |
6 | router.get("/", LabelController.get);
7 | router.post("/", LabelController.add);
8 | router.patch("/", LabelController.edit);
9 | router.delete("/", LabelController.del);
10 | export = router;
11 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/workflows/labeler.yml:
--------------------------------------------------------------------------------
1 | name: "Pull Request Labeler"
2 | on: pull_request
3 |
4 | jobs:
5 | job1:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: PR Labeler
9 | uses: actions/labeler@2.2.0
10 | with:
11 | repo-token: ${{ secrets.GITHUB_TOKEN }}
12 | configuration-path: .github/labeler.yml
13 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/workflows/reviewer_lottery.yml:
--------------------------------------------------------------------------------
1 | name: "Reviewer lottery"
2 | on:
3 | pull_request:
4 | types: [opened, ready_for_review, reopened]
5 |
6 | jobs:
7 | test:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v1
11 | - uses: uesteibar/reviewer-lottery@v1
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 |
--------------------------------------------------------------------------------
/BE/src/routes/comment.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import CommentController from "@controllers/comment";
3 |
4 | const router = express.Router();
5 |
6 | router.get("/:issueid", CommentController.get);
7 | router.post("/", CommentController.add);
8 | router.patch("/", CommentController.edit);
9 | router.delete("/", CommentController.del);
10 | export = router;
11 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueTracker.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.applesignin
6 |
7 | Default
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/BE/src/routes/milestone.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import Controller from "../controllers/milestone";
3 |
4 | const router = express.Router();
5 |
6 | router.get("/", Controller.get);
7 | router.post("/", Controller.add);
8 | router.patch("/", Controller.edit);
9 | router.delete("/", Controller.del);
10 | router.patch("/:id/state/:state", Controller.changeState);
11 | export = router;
12 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - trailing_whitespace
3 | - todo
4 | included:
5 | excluded:
6 | - Pods
7 | - IssueTracker/AppDelegate.swift
8 | - IssueTracker/SceneDelegate.swift
9 | identifier_name:
10 | min_length: 1
11 | large_tuple:
12 | - 3
13 | function_body_length: 200
14 | #line_length:
15 | # warning: 150
16 | #nesting:
17 | # type_level: 3
18 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | struct User: Codable {
11 | let userID: String?
12 | let password: String?
13 |
14 | enum CodingKeys: String, CodingKey {
15 | case userID = "user_id"
16 | case password
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/FE/src/pages/registerPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledRegisterPage = styled.div`
6 | display: flex;
7 | border: 1px dotted black;
8 | margin: 5px;
9 | `;
10 | function RegisterPage() {
11 | return RegisterPage;
12 | }
13 |
14 | export default hot(module)(RegisterPage);
15 |
--------------------------------------------------------------------------------
/FE/src/pages/issueCreatePage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledIssueCreatePage = styled.div`
6 | display: flex;
7 | border: 1px dotted black;
8 | margin: 5px;
9 | `;
10 | function IssueCreatePage() {
11 | return IssueCreatePage;
12 | }
13 |
14 | export default hot(module)(IssueCreatePage);
15 |
--------------------------------------------------------------------------------
/FE/src/pages/issueDetailPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledIssueDetailPage = styled.div`
6 | display: flex;
7 | border: 1px dotted black;
8 | margin: 5px;
9 | `;
10 | function IssueDetailPage() {
11 | return IssueDetailPage;
12 | }
13 |
14 | export default hot(module)(IssueDetailPage);
15 |
--------------------------------------------------------------------------------
/FE/src/util/axiosApi.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const HOST_ADDRESS = "http://101.101.210.34:3000";
4 |
5 | const axiosApi = async (url, method, data = null) => {
6 | const res = await axios({
7 | baseURL: HOST_ADDRESS,
8 | method,
9 | url,
10 | data,
11 | headers: {
12 | Authorization: `Bearer ${localStorage.getItem("token")}`,
13 | },
14 | });
15 | return res;
16 | };
17 |
18 | export default axiosApi;
19 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/UserInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserInfo.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | struct UserInfo: Codable {
11 | let state: String
12 | let token: String
13 | let userID: String
14 |
15 | enum CodingKeys: String, CodingKey {
16 | case state
17 | case token = "JWT"
18 | case userID = "user_id"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/SignIn/Models/RequestLogin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestLogin.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RequestLogin: Codable {
11 | let id: Int
12 | let state: String
13 | let jwt: String?
14 |
15 | enum CodingKeys: String, CodingKey {
16 | case id
17 | case state
18 | case jwt = "JWT"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/FE/src/pages/milestoneEditPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledMilestoneEditPage = styled.div`
6 | display: flex;
7 | border: 1px dotted black;
8 | margin: 5px;
9 | `;
10 | function MilestoneEditPage() {
11 | return MilestoneEditPage;
12 | }
13 |
14 | export default hot(module)(MilestoneEditPage);
15 |
--------------------------------------------------------------------------------
/FE/src/pages/milestoneCreatePage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledMilestoneCreatePage = styled.div`
6 | display: flex;
7 | border: 1px dotted black;
8 | margin: 5px;
9 | `;
10 | function MilestoneCreatePage() {
11 | return MilestoneCreatePage;
12 | }
13 |
14 | export default hot(module)(MilestoneCreatePage);
15 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Alamofire",
6 | "repositoryURL": "https://github.com/Alamofire/Alamofire.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "097e1f03166d49b31f824507fb85ad843b14fc13",
10 | "version": "5.3.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/FE/src/component/labelListPage/filter/labelFilter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledLabelFilter = styled.div`
6 | display: flex;
7 | border: 1px dotted black;
8 | margin: 5px;
9 | `;
10 | function LabelFilter() {
11 | return (
12 |
13 | StyledLabelFilter
14 |
15 | );
16 | }
17 |
18 | export default hot(module)(LabelFilter);
19 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Network/ContentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentType.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ContentType: CustomStringConvertible {
11 | case json
12 | case formEncode
13 |
14 | var description: String {
15 | switch self {
16 | case .formEncode: return "Application/json"
17 | case .json: return "application/x-www-form-urlencoded"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/FE/src/component/header/header.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledHeader = styled.div`
6 | display: flex;
7 | background-color: dimgrey;
8 | color: white;
9 | height: 10%;
10 | margin: 0px;
11 | font-weight: bold;
12 | justify-content: center;
13 | align-items: center;
14 | `;
15 | function Header() {
16 | return 🔥IssueTracker🔥;
17 | }
18 |
19 | export default hot(module)(Header);
20 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/progressbar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledProgressbar = styled.div`
6 | width: 100%;
7 | background-color: #b2bec3;
8 | `;
9 |
10 | function Progressbar({ percent }) {
11 | return (
12 |
13 |
14 |
15 | );
16 | }
17 | export default hot(module)(Progressbar);
18 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Assets.xcassets/apple-camera.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "apple-camera.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "apple-camera@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "apple-camera@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Network/HTTPMethod.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPMethod.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | enum HTTPMethod: CustomStringConvertible {
11 | case get
12 | case post
13 | case patch
14 | case delete
15 |
16 | var description: String {
17 | switch self {
18 | case .get: return "GET"
19 | case .post: return "POST"
20 | case .patch: return "PATCH"
21 | case .delete: return "DELETE"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Network/HTTPHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPHeader.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | enum HTTPHeader: CustomStringConvertible {
11 | case authentication
12 | case contentType
13 | case acceptType
14 |
15 | var description: String {
16 | switch self {
17 | case .authentication: return "Authorization"
18 | case .contentType: return "Accept-Encoding"
19 | case .acceptType: return "Content-Type"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTrackerTests/SignInTests/SignInEndPointTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignInEndPointTest.swift
3 | // IssueTrackerTests
4 | //
5 | // Created by ParkJaeHyun on 2020/10/28.
6 | //
7 |
8 | import XCTest
9 | @testable import IssueTracker
10 |
11 | class SignInEndPointTest: XCTestCase {
12 | func test_signIn_success() {
13 | //Given
14 |
15 | //When
16 |
17 | //Then
18 | }
19 |
20 | func test_asURLRequest_with_userMock_success() {
21 | //Given
22 |
23 | //When
24 |
25 | //Then
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/AllGetUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AllGetUser.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/12.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias AllGetUserList = [AllGetUser]
11 |
12 | struct AllGetUser: Codable {
13 | let id: Int
14 | let loginID, password: String
15 | let img: String
16 | let createdAt: String
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case id
20 | case loginID = "login_id"
21 | case password, img
22 | case createdAt = "created_at"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/buttons/editButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import axiosApi from "../../../util/axiosApi";
5 |
6 | const StylednewMilestoneButton = styled.button`
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | width: 60px;
11 | color: black;
12 | font-weight: bold;
13 | margin: 5px 0px;
14 | `;
15 | function EditMilestone({ id }) {
16 | return Edit;
17 | }
18 |
19 | export default hot(module)(EditMilestone);
20 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/Milestone.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Milestone.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias MilestoneList = [Milestone]
11 |
12 | struct Milestone: Codable {
13 | let id: Int
14 | let name: String
15 | let description: String
16 | let dueDate: String
17 | let createdAt: String
18 | let state: Int
19 |
20 | enum CodingKeys: String, CodingKey {
21 | case id, name, description, state
22 | case dueDate = "due_date"
23 | case createdAt = "created_at"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/Date+timeAgoDisplay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/13.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Date {
11 | /// Date를 특정 형식에 맞는 String으로 변환
12 | ///
13 | /// 함수를 확장하여 다양하게 사용 가능
14 | ///
15 | /// - Returns: String
16 | func timeAgoDisplay() -> String {
17 | let formatter = RelativeDateTimeFormatter()
18 | formatter.unitsStyle = .full
19 | formatter.dateTimeStyle = .numeric
20 | return formatter.localizedString(for: self, relativeTo: Date())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/String+asURL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+asURL.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// String 타입 url을 URL타입으로 변환
12 | ///```
13 | ///let url = "http://asURL.com"
14 | ///let urlType = url.asURL // URL타입으로 반환
15 | ///```
16 | /// - Throws: NetworkError.invalidURL
17 | /// - Returns: URL타입
18 | func asURL() throws -> URL {
19 | guard let url = URL(string: self) else { throw NetworkError.invalidURL }
20 |
21 | return url
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BE/src/routes/issue.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | import IssueController from "@controllers/issue";
3 | import authController from "@controllers/auth";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/",authController.authCheck, IssueController.get);
8 | router.post("/", IssueController.add);
9 | router.patch("/", IssueController.edit);
10 | router.delete("/", IssueController.del);
11 | router.patch("/:id/state/:state", IssueController.changeState);
12 |
13 | router.get("/filter/state/:state/author/:author/assignee/:assignee/comment/:comment", IssueController.getFilter);
14 | export = router;
15 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/buttons/openFilterBtn.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 |
6 | const StylednewMilestoneButton = styled.button`
7 | display: flex;
8 | color: black;
9 | font-weight: bold;
10 | margin: 5px 0px;
11 | `;
12 | function OpenMilestoneButton(props) {
13 | return (
14 |
15 | {props.open} Open
16 |
17 | );
18 | }
19 |
20 | export default hot(module)(OpenMilestoneButton);
21 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/buttons/closeFilterBtn.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 |
6 | const StylednewMilestoneButton = styled.button`
7 | display: flex;
8 | color: black;
9 | font-weight: bold;
10 | margin: 5px 0px;
11 | `;
12 | function ClosedMilestoneButton(props) {
13 | return (
14 |
15 | {props.close} Closed
16 |
17 | );
18 | }
19 |
20 | export default hot(module)(ClosedMilestoneButton);
21 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/Label.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Label.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias LabelList = [Label]
11 |
12 | struct Label: Codable {
13 | let id: Int?
14 | let issueID: Int?
15 | let labelID: Int?
16 | let name: String
17 | let description: String
18 | let color: String
19 | let createdAt: String
20 |
21 | enum CodingKeys: String, CodingKey {
22 | case id, name, description, color
23 | case issueID = "issue_id"
24 | case labelID = "label_id"
25 | case createdAt = "created_at"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/node.js_CI.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches: [ web_develop ]
6 | pull_request:
7 | branches: [ web_release ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [ 12.x ]
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 |
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v1
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 |
25 | - name: node
26 | working-directory: ./BE
27 | run: |
28 | npm ci
29 | npm run build --if-present
30 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Label/LabelPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelPresenter.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LabelPresentationLogic {
11 | func presentFetchedLabels(labels: LabelList)
12 | }
13 |
14 | class LabelPresenter: LabelPresentationLogic {
15 | weak var viewController: LabelDisplayLogic?
16 |
17 | func presentFetchedLabels(labels: LabelList) {
18 | DispatchQueue.main.async { [weak self] in
19 | let viewModel = labels.map { LabelViewModel(label: $0) }
20 | self?.viewController?.displayFetchedLabels(viewModel: viewModel)}
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/Assignee.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Assignee.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias AssigneeList = [Assignee]
11 |
12 | struct Assignee: Codable {
13 | let id: Int
14 | let issueID: Int
15 | let userID: Int
16 | let loginID: String
17 | let password: String
18 | let img: String
19 | let createdAt: String
20 |
21 | enum CodingKeys: String, CodingKey {
22 | case id, password, img
23 | case issueID = "issue_id"
24 | case userID = "user_id"
25 | case loginID = "login_id"
26 | case createdAt = "created_at"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueDetail/IssueDetailPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailPresenter.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol IssueDetailPresentationLogic {
11 | func presentFetchedComments(issues: DetailCommentList)
12 | }
13 |
14 | final class IssueDetailPresenter: IssueDetailPresentationLogic {
15 | weak var viewController: IssueDetailDisplayLogic?
16 |
17 | func presentFetchedComments(issues: DetailCommentList) {
18 | let viewModel = issues.map({ IssueDetailViewModel(commentList: $0) })
19 | viewController?.displayFetchedComments(viewModel: viewModel)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Label/Views/LabelCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelCollectionViewCell.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | class LabelCollectionViewCell: UICollectionViewListCell {
11 |
12 | @IBOutlet weak var labelStackView: UIStackView!
13 | @IBOutlet weak var descriptionLabel: UILabel!
14 |
15 | func configure(viewModel: LabelViewModel) {
16 | labelStackView.arrangedSubviews.forEach {
17 | $0.removeFromSuperview()
18 | }
19 | labelStackView.addArrangedSubview(viewModel.labelButton)
20 | descriptionLabel.text = viewModel.description
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/information.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | const StyledInformation = styled.div`
6 | margin: 5px 0px;
7 | `;
8 |
9 | const InformationSpan = styled.span`
10 | font-size: 14px;
11 | font-weight: 600;
12 | margin: 0px 7px;
13 | `;
14 |
15 | function Information({ percent, open, close }) {
16 | return (
17 |
18 | {percent}%
19 | {open}open
20 | {close}close
21 |
22 | );
23 | }
24 |
25 | export default hot(module)(Information);
26 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueList/Models/IssueListModelController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueListModelController.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/03.
6 | //
7 |
8 | import Foundation
9 |
10 | final class IssueListModelController {
11 | func filteredBasedOnTitle(with filter: String, model: [IssueListViewModel]) -> [IssueListViewModel] {
12 | let filtered = model.filter { $0.contains(filter) }
13 | return filtered
14 | }
15 |
16 | func add(model: IssueListViewModel, to issueList: [IssueListViewModel]) -> [IssueListViewModel] {
17 | var issueListCopy = issueList
18 | issueListCopy.append(model)
19 | return issueListCopy
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueList/IssueListPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueListPresenter.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol IssueListPresentationLogic {
11 | func presentFetchedIssues(issues: IssueList)
12 | }
13 |
14 | final class IssueListPresenter: IssueListPresentationLogic {
15 | weak var viewController: IssueListDisplayLogic?
16 |
17 | func presentFetchedIssues(issues: IssueList) {
18 | DispatchQueue.main.async { [weak self] in
19 | let viewModel = issues.map { IssueListViewModel(issue: $0) }
20 | self?.viewController?.displayFetchedIssues(viewModel: viewModel)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Edit/Models/IssueDetailEditEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailEditEndPoint.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/12.
6 | //
7 |
8 | import Foundation
9 |
10 | enum IssueDetailEditEndPoint: APIConfiguration {
11 | case getAllUser
12 |
13 | var method: HTTPMethod {
14 | switch self {
15 | case .getAllUser:
16 | return .get
17 | }
18 | }
19 |
20 | var path: String {
21 | switch self {
22 | case .getAllUser:
23 | return "/auth/alluser"
24 | }
25 | }
26 |
27 | var body: Data? {
28 | switch self {
29 | case .getAllUser:
30 | return nil
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTrackerTests/IssueTrackerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueTrackerTests.swift
3 | // IssueTrackerTests
4 | //
5 | // Created by ParkJaeHyun on 2020/10/26.
6 | //
7 |
8 | import XCTest
9 | @testable import IssueTracker
10 |
11 | class IssueTrackerTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 |
15 | }
16 |
17 | override func tearDownWithError() throws {
18 |
19 | }
20 |
21 | func testTrueExample() throws {
22 | XCTAssertTrue(true)
23 | }
24 |
25 | func testFalseExample() throws {
26 | XCTAssertFalse(false)
27 | }
28 |
29 | func testPerformanceExample() throws {
30 | self.measure {
31 |
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/BE/README.md:
--------------------------------------------------------------------------------
1 | # IssueTracker-3 Back End
2 | [](https://github.com/boostcamp-2020/IssueTracker-3/releases)
3 | [](https://github.com/boostcamp-2020/IssueTracker-3/actions)
4 |
5 |
6 | # 디렉터리 구조
7 | GeekyAnts님의 [express-typescript boilerplate](https://github.com/GeekyAnts/express-typescript) 를 참고했습니다.
8 | ```
9 | 📂BE
10 | ├build
11 | ├src
12 | │ ├📂controllers
13 | │ ├📂interfaces
14 | │ ├📂models
15 | │ ├📂providers
16 | │ ├📂routes
17 | │ ├app.ts
18 | │ └index.ts
19 | ├.env
20 | ├.eslintrc.json
21 | ├.gitignore
22 | ├.prettierrc
23 | ├package.json
24 | ├README.md
25 | └tsconfig.json
26 | ```
27 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/URL+searchToken.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+searchToken.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 | /// URL에서 tokenQuery에 해당하는 token을 찾아줌
12 | ///
13 | /// ```
14 | /// callBackURL.searchToken(of: self.tokenQuery)
15 | ///
16 | /// ```
17 | ///
18 | /// - Returns: String
19 | func searchToken(of tokenQuery: String) -> String {
20 | guard let queryItems = URLComponents(string: self.absoluteString)?.queryItems,
21 | let token = queryItems.first(where: { $0.name == tokenQuery })?.value
22 | else {
23 | return ""
24 | }
25 | return token
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/buttons/moveToCreateIssueButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 |
6 | const StyledMoveToCreateIssueButton = styled.button`
7 | display: flex;
8 | border: 1px solid forestgreen;
9 | margin: 5px;
10 | color: white;
11 | border-radius: 5px;
12 | background-color: #33cc33;
13 | `;
14 | function MoveToCreateIssueButton() {
15 | return (
16 |
17 | New issue
18 |
19 | );
20 | }
21 |
22 | export default hot(module)(MoveToCreateIssueButton);
23 |
--------------------------------------------------------------------------------
/FE/src/component/labelListPage/buttons/moveToCreateLabelButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 |
6 | const StyledMoveToCreateLabelButton = styled.button`
7 | display: flex;
8 | border: 1px solid forestgreen;
9 | margin: 5px;
10 | margin-top: 5px;
11 | margin-bottom: 5px;
12 | margin-left: 400px;
13 | margin-right: 5px;
14 | color: white;
15 | border-radius: 5px;
16 | background-color: #33cc33;
17 | `;
18 | function MoveToCreateLabelButton(prop) {
19 | return New Label ;
20 | }
21 |
22 | export default hot(module)(MoveToCreateLabelButton);
23 |
--------------------------------------------------------------------------------
/FE/src/component/labelListPage/buttons/moveToLabelButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 |
6 | const StyledMoveToLabelButton = styled.button`
7 | display: flex;
8 | border: 1px solid gray;
9 | background-color: blue;
10 | color: white;
11 | font-weight: bold;
12 | margin: 5px 0px;
13 | border-top-left-radius: 4px;
14 | border-bottom-left-radius: 4px;
15 | `;
16 | function MoveToLabelButton() {
17 | return (
18 |
19 | 라벨
20 |
21 | );
22 | }
23 |
24 | export default hot(module)(MoveToLabelButton);
25 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/Encodable+encoded.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Encodable+encoded.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Encodable {
11 | /// Encodable 객체를 인코딩 해줌
12 | ///
13 | /// ```
14 | /// guard let data = try? data.encoded() else {
15 | /// return
16 | /// }
17 | /// ```
18 | ///
19 | /// - Returns: Data
20 | func encoded() -> Data {
21 | let encoder = JSONEncoder()
22 | encoder.outputFormatting = .prettyPrinted
23 | encoder.keyEncodingStrategy = .convertToSnakeCase
24 | guard let encodedData = try? encoder.encode(self) else { return Data() }
25 | return encodedData
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Milestone/MilestonePresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MilestonePresenter.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol MilestonePresentationLogic {
11 | func presentFetchedMilestones(milestones: MilestoneList)
12 | }
13 |
14 | final class MilestonePresenter: MilestonePresentationLogic {
15 | weak var viewController: MilestoneDisplayLogic?
16 |
17 | func presentFetchedMilestones(milestones: MilestoneList) {
18 | DispatchQueue.main.async { [weak self] in
19 | let viewModel = milestones.map { MilestoneViewModel(milestone: $0) }
20 | self?.viewController?.displayFetchedMilestones(viewModel: viewModel)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/buttons/newButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 |
6 | const StylednewMilestoneButton = styled.button`
7 | display: flex;
8 | border: 1px solid gray;
9 | background-color: green;
10 | color: white;
11 | font-weight: bold;
12 | margin: 5px 0px;
13 | border-top-left-radius: 4px;
14 | border-bottom-left-radius: 4px;
15 | `;
16 | function NewMilestone() {
17 | return (
18 |
19 | New Milestone
20 |
21 | );
22 | }
23 |
24 | export default hot(module)(NewMilestone);
25 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/String+toDate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String + toDate.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/04.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// Date 타입으로 변경
12 | ///
13 | ///```
14 | ///let str = "2020-11-01T15:00:00.000Z"
15 | ///str.toDate()
16 | ///```
17 | ///
18 | /// - Returns: "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 형식의 Date 타입
19 | func toDate() -> Date? {
20 | let dateFormatter = DateFormatter()
21 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
22 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
23 | //"yyyy-MM-dd'T'HH:mm:ssZ"
24 | let date: Date? = dateFormatter.date(from: self)
25 | return date
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FE/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | },
5 | "extends": ["airbnb-base","plugin:prettier/recommended", "react-app"],
6 | "plugins" : ["prettier"],
7 | "ignorePatterns": ["node_modules/","build/"],
8 | "rules": {
9 |
10 | "prettier/prettier": ["error",{ "endOfLine": "auto"}],
11 |
12 | "import/extensions": [
13 | "error",
14 | "ignorePackages",
15 | {
16 | "js": "never",
17 | "jsx": "never",
18 | "ts": "never",
19 | "tsx": "never"
20 | }
21 | ],
22 | "import/no-unresolved": "off"
23 | },
24 | "settings": {
25 | "import/resolver": {
26 | "node": {
27 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
28 | }
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIColor+random.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor + hex + random.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/03.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 | /// UIColor 랜덤으로 생성
12 | ///
13 | /// - 기존에 있던 UIColor(red:, green:, blue:, alpha:) init 메소드에서 r, g, b에만 random 메소드로 값을 지정
14 | /// - alpha는 1로 고정
15 | ///
16 | ///```
17 | ///let color = UIColor().random()
18 | ///```
19 | ///
20 | /// - Returns: Random UIColor
21 | func random() -> UIColor {
22 | return UIColor(red: .random(in: 0...1),
23 | green: .random(in: 0...1),
24 | blue: .random(in: 0...1),
25 | alpha: 1)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/DateFormatter+format.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateFormatter+format.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | extension DateFormatter {
11 | /// Date를 특정 형식에 맞는 String으로 변환
12 | ///
13 | /// 함수를 확장하여 다양하게 사용 가능
14 | ///
15 | ///```
16 | ///let dateToString = DataFormatter(Date())
17 | ///
18 | ///```
19 | ///
20 | /// - Returns: "YYYY년 M월 d일까지" 형식의 String 타입
21 | static func format(_ date: Date) -> String {
22 | let dateFormatter = DateFormatter()
23 | dateFormatter.locale = Locale(identifier: "ko_kr")
24 | dateFormatter.dateFormat = "YYYY년 M월 d일까지"
25 | return dateFormatter.string(from: date)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIColor+visibleTextColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+visibleTextColor.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/09.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 | /// 배경색 기준으로 Text Color (black & white) 정해줌
12 | ///```
13 | ///let uiColor = UIColor.black
14 | ///let textColor = uiColor.visibleTextColor // "UIColor.white"
15 | ///```
16 | var visibleTextColor: UIColor {
17 | let ciColor = CIColor(color: self)
18 | let red = ciColor.red
19 | let green = ciColor.green
20 | let blue = ciColor.blue
21 | let yiq = ((red * 299) + (green * 587) + (blue * 114))/1000
22 | return yiq >= 128/255 ? UIColor.black : UIColor.white
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "MarkdownView",
6 | "repositoryURL": "https://github.com/keitaoouchi/MarkdownView.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "b88cd53afe4c8bb42d40e0141be03922d1d3c6c9",
10 | "version": "1.7.1"
11 | }
12 | },
13 | {
14 | "package": "SwiftyMarkdown",
15 | "repositoryURL": "https://github.com/SimonFairbairn/SwiftyMarkdown.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "5b0a1e76332a633726f9f9a00b4bbd840166bccf",
19 | "version": "1.2.3"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/element/issuelist.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import Issue from "@component/issueListPage/element/issue";
5 |
6 | const StyledIssueList = styled.div`
7 | display: flex;
8 | width: 100%;
9 | border: 1px solid gray;
10 | margin: 5px;
11 | flex-direction: column;
12 | `;
13 |
14 | function IssueList(props) {
15 | const { issues, setChecked, checked } = props;
16 | return (
17 | <>
18 |
19 | {issues?.map((issue, index) => (
20 |
21 | ))}
22 |
23 | >
24 | );
25 | }
26 |
27 | export default hot(module)(IssueList);
28 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/10/26.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | return true
15 | }
16 |
17 | // MARK: UISceneSession Lifecycle
18 |
19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIView+shake.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+shake.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/11.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 | func shake(count: Float = 2, for duration: TimeInterval = 0.15, withTranslation translation: Float = 5) {
12 | let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
13 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
14 | animation.repeatCount = count
15 | animation.duration = duration / TimeInterval(animation.repeatCount)
16 | animation.autoreverses = true
17 | animation.values = [translation, -translation]
18 | layer.add(animation, forKey: "shake")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Network/NetworkError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkError.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | enum NetworkError: Error {
11 |
12 | // MARK: Request Error
13 |
14 | case invalidToken
15 | case invalidURL
16 | case requestFailure(message: String)
17 |
18 | // MARK: Response Error
19 |
20 | case invalidResponse(message: String)
21 | case invalidData(message: String)
22 |
23 | // MARK: Status Code
24 |
25 | case informational(message: String)
26 | case redirection(message: String)
27 | case clientError(message: String)
28 | case serverError(message: String)
29 |
30 | // MARK: Token
31 |
32 | case tokenExpiration(message: String)
33 | }
34 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/10/26.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene,
15 | willConnectTo session: UISceneSession,
16 | options connectionOptions: UIScene.ConnectionOptions)
17 | {
18 | if NetworkService.token != "" {
19 | let storyboard = UIStoryboard(name: "Main", bundle: nil)
20 | window?.rootViewController = storyboard.instantiateViewController(withIdentifier: "UITabBarController")
21 | }
22 |
23 | guard let _ = (scene as? UIWindowScene) else { return }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/Data+decoded.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Data+decoded.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Data {
11 | /// Json Data를 디코딩 해줌
12 | ///
13 | /// Generics T : Decodable
14 | /// ```
15 | /// guard let decodedData: IssueList = try? data.decoded() else {
16 | /// return
17 | /// }
18 | ///
19 | /// guard let decodedData = try? data.decoded() as? IssueList else {
20 | /// return
21 | /// }
22 | /// ```
23 | /// - Returns: T
24 | func decoded() throws -> T {
25 | let decoder = JSONDecoder()
26 | // decoder.keyDecodingStrategy = .convertFromSnakeCase
27 | return try decoder.decode(T.self, from: self)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/FE/src/util/useRequest.jsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { useEffect, useState } from "react";
3 |
4 | const HOST_ADDRESS = "http://101.101.210.34:3000";
5 |
6 | function useRequest(url, method, data = null) {
7 | const [loading, setLoading] = useState(false);
8 | const [response, setResponse] = useState(null);
9 | const [error, setError] = useState(null);
10 |
11 | useEffect(async () => {
12 | setError(null);
13 | try {
14 | setLoading(true);
15 | const res = await axios({
16 | baseURL: HOST_ADDRESS,
17 | method,
18 | url,
19 | data,
20 | });
21 | setResponse(res);
22 | } catch (e) {
23 | setError(e);
24 | }
25 | setLoading(false);
26 | }, [url]);
27 | return [response, loading, error];
28 | }
29 |
30 | export default useRequest;
31 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Edit/Models/IssueDetailEditViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailEditViewModel.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/12.
6 | //
7 |
8 | import Foundation
9 |
10 | struct IssueDetailEditViewModel: Hashable {
11 | let title: String
12 | let color: String?
13 | let assigneeID: Int?
14 | let labelID: Int?
15 | let milestoneID: Int?
16 | let identifier = UUID()
17 | let labels: CustomButtonView?
18 | let milestone: CustomButtonView?
19 | let assignee: CustomButtonView?
20 |
21 | func hash(into hasher: inout Hasher) {
22 | hasher.combine(identifier)
23 | }
24 |
25 | static func == (lhs: IssueDetailEditViewModel, rhs: IssueDetailEditViewModel) -> Bool {
26 | return lhs.identifier == rhs.identifier
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/buttons/closeButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import axiosApi from "../../../util/axiosApi";
5 |
6 | const StylednewMilestoneButton = styled.button`
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | width: 60px;
11 | color: black;
12 | font-weight: bold;
13 | margin: 5px 0px;
14 | `;
15 | function CloseMilestone({ id }) {
16 | const closeMilestone = async (e) => {
17 | const result = await axiosApi(`http://localhost:3000/milestone/${id}/state/0`, "PATCH");
18 | if (result === 200) console.log(`closed`);
19 | };
20 | return Close;
21 | }
22 |
23 | export default hot(module)(CloseMilestone);
24 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/list/milestoneListHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import OpenMilestoneButton from "../buttons/openFilterBtn";
5 | import ClosedMilestoneButton from "../buttons/closeFilterBtn";
6 |
7 | const StylednewListHeader = styled.div`
8 | display: flex;
9 | align-items: center;
10 | width: 100%;
11 | height: 50px;
12 | border: 1px dotted black;
13 | box-sizing: border-box;
14 | background-color: #dfe6e9;
15 | padding: 10px;
16 | `;
17 |
18 | function ListHeader(props) {
19 | return (
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default hot(module)(ListHeader);
28 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTrackerTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/FE/src/component/loginPage/buttons/loginGithubButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import axiosApi from "@util/axiosApi";
5 |
6 | const StyledGithubButton = styled.button`
7 | display: flex;
8 | border: 1px dotted black;
9 | margin: 5px;
10 | `;
11 | function GithubButton() {
12 | const githubLogin = async () => {
13 | const res = await axiosApi("https://github.com/login/oauth/authorize?client_id=f3f153d6be2389b2b220&redirect_uri=http://101.101.210.34/callback", "GET");
14 | };
15 | return (
16 | 깃허브로 로그인 하기
17 | );
18 | }
19 |
20 | export default hot(module)(GithubButton);
21 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/PropertyWrapper/KeyChain.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyChain.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | @propertyWrapper
11 | struct KeyChain {
12 | let key: String
13 | let defaultValue: String
14 |
15 | init(key: String, defaultValue: String = "") {
16 | self.key = key
17 | self.defaultValue = defaultValue
18 | }
19 |
20 | var wrappedValue: String {
21 | get {
22 | return KeychainAccess.shared.get(forAccountKey: key) ?? defaultValue
23 | }
24 | set {
25 | do {
26 | try KeychainAccess.shared.set(newValue, forAccountKey: key)
27 | } catch {
28 | debugPrint(error.localizedDescription)
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Label/Models/LabelViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelViewModel.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | class LabelViewModel: Hashable {
11 | let id: Int?
12 | let description: String
13 | let labelButton: CustomButtonView
14 | let identifier = UUID()
15 |
16 | init(label: Label) {
17 | self.id = label.id
18 | self.description = label.description
19 | self.labelButton = CustomButtonView(type: .label, text: label.name, color: label.color)
20 | }
21 |
22 | func hash(into hasher: inout Hasher) {
23 | hasher.combine(identifier)
24 | }
25 |
26 | static func == (lhs: LabelViewModel, rhs: LabelViewModel) -> Bool {
27 | return lhs.identifier == rhs.identifier
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/FE/src/component/labelListPage/buttons/moveToMilestoneButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 |
6 | const StyledMoveToMaileStoneButton = styled.button`
7 | display: flex;
8 | border: 1px solid gray;
9 | background-color: white;
10 | font-weight: bold;
11 | margin: 5px 0px;
12 | border-top-right-radius: 4px;
13 | border-bottom-right-radius: 4px;
14 |
15 | &:hover {
16 | background-color: aquamarine;
17 | }
18 | `;
19 | function MoveToMaileStoneButton() {
20 | return (
21 |
22 | 마일스톤
23 |
24 | );
25 | }
26 |
27 | export default hot(module)(MoveToMaileStoneButton);
28 |
--------------------------------------------------------------------------------
/FE/src/component/loginPage/form/loginform.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 |
4 | const LoginForm = (props) => {
5 | const onNameHandler = (event) => {
6 | const name = event.target.value;
7 | const { password } = props.inputData;
8 | props.setInput({ name, password });
9 | };
10 | const onPasswordHandler = (event) => {
11 | const { name } = props.inputData;
12 | const password = event.target.value;
13 | props.setInput({ name, password });
14 | };
15 | return (
16 |
22 | );
23 | };
24 |
25 | export default hot(module)(LoginForm);
26 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/buttons/deleteButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import axiosApi from "../../../util/axiosApi";
5 |
6 | const StylednewMilestoneButton = styled.button`
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | width: 60px;
11 | color: black;
12 | font-weight: bold;
13 | margin: 5px 0px;
14 | `;
15 | function DeleteMilestone({ id }) {
16 | const deleteMilestone = async (e) => {
17 | console.log(`click`);
18 | const result = await axiosApi("http://localhost:3000/milestone", "DELETE", { id });
19 | if (result.status === 200) console.log("성공");
20 | };
21 | return Delete;
22 | }
23 |
24 | export default hot(module)(DeleteMilestone);
25 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIVIew+snapshot.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIVIew + snapshot.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/03.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 | /// View를 UIImage로 생성
12 | ///
13 | /// 지정한 view를 이미지로 만들어줌
14 | /// ```
15 | /// let uiImage: UIImage = view.snapshot()
16 | /// ```
17 | /// - Returns: UIImage()
18 | func snapshot() -> UIImage {
19 | UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, UIScreen.main.scale)
20 | guard let currentContext = UIGraphicsGetCurrentContext() else { return UIImage() }
21 | self.layer.render(in: currentContext)
22 | guard let img = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() }
23 | UIGraphicsEndImageContext()
24 | return img
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Setting/SettingViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingViewController.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/02.
6 | //
7 |
8 | import UIKit
9 |
10 | class SettingViewController: UIViewController {
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 | }
15 |
16 | @IBAction func signOutTouched(_ sender: UIButton) {
17 | try? KeychainAccess.shared.removeAll()
18 |
19 | let storyboard = UIStoryboard(name: "Main", bundle: nil)
20 | guard let navigationController = storyboard
21 | .instantiateViewController(withIdentifier: "AuthenticationNavigationController")
22 | as? UINavigationController else {
23 | return
24 | }
25 | self.view.window?.rootViewController = navigationController
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIColor+hexString.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+hexString.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/09.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 | /// UIColor를 hexString으로 변경
12 | ///
13 | ///```
14 | ///let color = UIColor.red
15 | ///let hex = color.hexString // "#FF2600"
16 | ///```
17 | var hexString: String {
18 | let components = self.cgColor.components
19 | let red: CGFloat = components?[0] ?? 0.0
20 | let green: CGFloat = components?[1] ?? 0.0
21 | let blue: CGFloat = components?[2] ?? 0.0
22 | return String(
23 | format: "#%02lX%02lX%02lX",
24 | lroundf(Float(red * 255)),
25 | lroundf(Float(green * 255)),
26 | lroundf(Float(blue * 255))
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/BE/src/providers/database.ts:
--------------------------------------------------------------------------------
1 | import mysql2 from "mysql2/promise";
2 | import dotenv from "dotenv";
3 | import path from "path";
4 |
5 | dotenv.config({ path: path.join(__dirname, "../../.env") });
6 |
7 | class DB {
8 | pool: mysql2.Pool;
9 |
10 | constructor() {
11 | this.pool = mysql2.createPool({
12 | host: process.env.DATABASE_HOST,
13 | port: Number(process.env.DATABASE_PORT),
14 | user: process.env.DATABASE_USER,
15 | password: process.env.DATABASE_PASSWORD,
16 | database: process.env.DATABASE_NAME,
17 | connectionLimit: 10,
18 | });
19 | }
20 |
21 | async query(str: string, params?: T): Promise {
22 | try {
23 | const result = await this.pool.query(str, params);
24 | return result;
25 | } catch (err) {
26 | return err;
27 | }
28 | }
29 | }
30 |
31 | const db = new DB();
32 |
33 | export default db;
34 |
--------------------------------------------------------------------------------
/BE/src/utils/filter.ts:
--------------------------------------------------------------------------------
1 | const stateFilter = (state1: boolean, state2: boolean): boolean => {
2 | return state1 === state2;
3 | };
4 |
5 | const userFilter = (id1: number, id2: number): boolean => {
6 | return id1 === id2;
7 | };
8 |
9 | const assigneeFilter = (assignees: Array, id: number): boolean => {
10 | return !!assignees.filter((assignee) => assignee.user_id === id).length;
11 | };
12 |
13 | const commentFilter = (comments: Array, id: number): boolean => {
14 | return !!comments.filter((comment) => comment.user_id === id).length;
15 | };
16 |
17 | const nullFilter = (value: any): any => {
18 | // eslint-disable-next-line no-restricted-syntax
19 | for (const property in value) {
20 | if (value[property] === null) value[property] = "";
21 | }
22 | return value;
23 | };
24 |
25 | export default { stateFilter, userFilter, assigneeFilter, commentFilter, nullFilter };
26 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIViewController+hideKeyboardWhenTapped.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+hideKeyboard.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/12.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIViewController {
11 | /// TextField 등 editing 상태에서 다른 아무 곳을 tap하면 keyboard를 숨김
12 | ///```
13 | /// override func viewDidLoad() {
14 | /// super.viewDidLoad()
15 | /// hideKeyboardWhenTappedAround()
16 | /// }
17 | ///```
18 | func hideKeyboardWhenTappedAround() {
19 | let tap = UITapGestureRecognizer(target: self,
20 | action: #selector(UIViewController.dismissKeyboard))
21 | tap.cancelsTouchesInView = false
22 | view.addGestureRecognizer(tap)
23 | }
24 |
25 | @objc func dismissKeyboard() {
26 | view.endEditing(true)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/BE/src/routes/auth.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import authController from "@controllers/auth";
3 | import userController from "@controllers/user";
4 | import auth from "@controllers/auth";
5 |
6 | const router = express.Router();
7 |
8 | router.get("/",authController.authCheck, authController.getUser);
9 | router.post("/login", authController.login);
10 |
11 | router.get("/github", authController.github);
12 | router.get("/alluser", authController.getAllUser);
13 | router.get("/github/callback", authController.github);
14 | router.get("/github/loginFail", authController.githubLoginFail);
15 | router.post("/github/token", authController.githubToken);
16 | router.post("/github/web", authController.githubWeb);
17 |
18 |
19 | router.post("/apple", authController.apple);
20 |
21 | router.get("/logout", authController.logout);
22 | router.post("/register", userController.add);
23 |
24 | export = router;
25 |
--------------------------------------------------------------------------------
/FE/src/pages/callback.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import axios from 'axios';
3 | import qs from 'qs';
4 |
5 | function Callback({ history, location }) {
6 | useEffect(() => {
7 | async function getToken() {
8 | const { code } = qs.parse(location.search, {
9 | ignoreQueryPrefix: true,
10 | });
11 |
12 | try {
13 | const result = await axios.post(`http://101.101.210.34:3000/auth/github/web`, {
14 | code,
15 | });
16 | console.log(result.data.JW);
17 | // 유저 JWT 토큰을 저장합니다.
18 | localStorage.setItem('token', result.data.JWT);
19 | window.location.href="/";
20 | } catch (error) {
21 | history.push('/error'); // api요청이 실패했을때 애러 핸들링 페이지
22 | }
23 | }
24 |
25 | getToken();
26 | }, [location, history]);
27 | return null; // 이 부분에 로딩바와 같은 페이지를 렌더링 해도 좋아요.
28 | }
29 |
30 | export default Callback;
31 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Edit/Views/EditTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditTableViewCell.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/12.
6 | //
7 |
8 | import UIKit
9 |
10 | final class EditTableViewCell: UITableViewCell {
11 |
12 | @IBOutlet weak var button: UIButton!
13 | @IBOutlet weak var stackView: UIStackView!
14 |
15 | enum ButtonType {
16 | case seleted
17 | case list
18 | }
19 |
20 | func configure(button: CustomButtonView) {
21 | stackView.addArrangedSubview(button)
22 | }
23 |
24 | func changeButtonImage(buttonType: ButtonType) {
25 | switch buttonType {
26 | case .seleted:
27 | button.setImage(UIImage(systemName: "plus.circle"), for: .normal)
28 | case .list:
29 | button.setImage(UIImage(systemName: "minus.circle"), for: .normal)
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/BE/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "project": "./BE/tsconfig.json"
5 | },
6 | "env": {
7 | "node": true
8 | },
9 | "extends": ["airbnb-base","plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "prettier/@typescript-eslint"],
10 | "ignorePatterns": ["node_modules/","build/"],
11 | "rules": {
12 |
13 | "prettier/prettier": ["error",{ "endOfLine": "auto"}],
14 |
15 | "import/extensions": [
16 | "error",
17 | "ignorePackages",
18 | {
19 | "js": "never",
20 | "jsx": "never",
21 | "ts": "never",
22 | "tsx": "never"
23 | }
24 | ],
25 | "import/no-unresolved": "off"
26 | },
27 | "settings": {
28 | "import/resolver": {
29 | "node": {
30 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIView+IBInspectable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewExtension.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/10/27.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 | @IBInspectable
12 | var cornerRadius: CGFloat {
13 | get { return layer.cornerRadius }
14 | set { layer.cornerRadius = newValue }
15 | }
16 |
17 | @IBInspectable
18 | var borderWidth: CGFloat {
19 | get { return layer.borderWidth }
20 | set { layer.borderWidth = newValue }
21 | }
22 |
23 | @IBInspectable
24 | var borderColor: UIColor? {
25 | get {
26 | guard let color = layer.borderColor else { return nil }
27 | return UIColor(cgColor: color)
28 | }
29 | set {
30 | guard let color = newValue else {
31 | layer.borderColor = nil
32 | return
33 | }
34 | layer.borderColor = color.cgColor
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTrackerTests/IssueListTests/IssueListFilterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueListFilterTests.swift
3 | // IssueTrackerTests
4 | //
5 | // Created by ParkJaeHyun on 2020/11/03.
6 | //
7 |
8 | import XCTest
9 | @testable import IssueTracker
10 |
11 | class IssueListFilterTests: XCTestCase {
12 | func test_filtered_with_IssuesMock_Success() throws {
13 | // Given
14 | let issueMock = IssuesMock()
15 | let filteredIssues = issueMock.issues
16 | let controller = IssueListModelController()
17 |
18 | // When
19 | //controller.filtered(with filter: String)
20 | let filteredIssueListViewModel = controller.filteredBasedOnTitle(with: "test", model: filteredIssues)
21 |
22 | // Then
23 | XCTAssertEqual(filteredIssueListViewModel.count, 3)
24 | XCTAssertTrue(filteredIssueListViewModel.contains(where: { $0.title == "test1"}))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/FE/src/pages/loginPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import LoginSubmitButton from "../component/loginPage/buttons/loginSubmitButton";
5 | import LoginGithubButton from "../component/loginPage/buttons/loginGithubButton";
6 | import LoginFrom from "../component/loginPage/form/loginform";
7 |
8 | const StyledLoginPage = styled.div`
9 | display: flex;
10 | border: 1px dotted black;
11 | margin: 5px;
12 | `;
13 | function LoginPage(props) {
14 | const [input, setInput] = useState({ name: "", password: "" });
15 | return (
16 |
17 | LoginPage
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default hot(module)(LoginPage);
26 |
--------------------------------------------------------------------------------
/BE/src/controllers/event.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { Event } from "@interfaces/event";
3 | import EventModel from "@models/event";
4 | import HTTPCODE from "@utils/magicnumber";
5 |
6 | const get = async (req: Request, res: Response): Promise> => {
7 | const { issueid } = req.params;
8 | try {
9 | const result = await EventModel.select(Number(issueid));
10 | return res.json(result);
11 | } catch {
12 | return res.sendStatus(HTTPCODE.SERVER_ERR);
13 | }
14 | };
15 |
16 | const add = async (req: Request, res: Response): Promise> => {
17 | const event: Event = {
18 | id: null,
19 | issue_id: Number(req.params.issueid),
20 | user_id: req.body.user_id,
21 | log: req.body.log,
22 | created_at: new Date(),
23 | };
24 | const result = await EventModel.add(event);
25 | return res.sendStatus(result);
26 | };
27 |
28 | export default { get, add };
29 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/element/issue.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Checkbox } from "@material-ui/core";
5 |
6 | const StyledIssue = styled.div`
7 | border: 1px solid gray;
8 | border-radius: 1px;
9 | `;
10 |
11 | const Issue = (props) => {
12 | const { issue, checked, setChecked } = props;
13 | const onCheckboxHandler = (event) => {
14 | if (event.target.checked) {
15 | setChecked([...checked, issue.id]);
16 | } else {
17 | const checkout = [...checked];
18 | const idx = checkout.indexOf(issue.id);
19 | checkout.splice(idx, 1);
20 | setChecked(checkout);
21 | }
22 | };
23 | return (
24 | <>
25 |
26 |
27 | {issue.title}
28 |
29 | >
30 | );
31 | };
32 |
33 | export default hot(module)(Issue);
34 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/SignIn/Models/AppleUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppleUser.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/13.
6 | //
7 |
8 | import Foundation
9 |
10 | final class AppleUser {
11 | var id: String?
12 | var firstName: String?
13 | var lastName: String?
14 | var email: String?
15 | var password: String?
16 | var authorizationCode: String?
17 | var identityToken: String?
18 |
19 | init(_ id: String?,
20 | _ firstName: String?,
21 | _ lastName: String?,
22 | _ email: String?,
23 | _ password: String?,
24 | _ authorizationCode: String?,
25 | _ identityToken: String?) {
26 | self.id = id
27 | self.firstName = firstName
28 | self.lastName = lastName
29 | self.email = email
30 | self.password = password
31 | self.authorizationCode = authorizationCode
32 | self.identityToken = identityToken
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueList/IssueListEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueListEndPoint.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | enum IssueListEndPoint: APIConfiguration {
11 | case getIssues
12 | case changeState(Int, Int)
13 |
14 | var method: HTTPMethod {
15 | switch self {
16 | case .getIssues:
17 | return .get
18 | case .changeState:
19 | return .patch
20 | }
21 | }
22 |
23 | var path: String {
24 | switch self {
25 | case .getIssues:
26 | return "/issue"
27 | case .changeState(let id, let state):
28 | return "/issue/\(id)/state/\(state)"
29 | }
30 | }
31 |
32 | var body: Data? {
33 | switch self {
34 | case .getIssues:
35 | return nil
36 | case .changeState:
37 | return nil
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueDetail/Views/IssueDetailCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailCollectionViewCell.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/02.
6 | //
7 |
8 | import UIKit
9 | import SwiftyMarkdown
10 |
11 | class IssueDetailCollectionViewCell: UICollectionViewCell {
12 |
13 | @IBOutlet weak var authorLabel: UILabel!
14 | @IBOutlet weak var timeLabel: UILabel!
15 | @IBOutlet weak var descriptionLabel: UILabel!
16 | @IBOutlet weak var profileImage: UIImageView!
17 |
18 | func configure(of item: IssueDetailViewModel) {
19 | timeLabel.text = item.createdAt
20 | descriptionLabel.attributedText = convertMarkdownText(of: item.body)
21 | profileImage.image = UIImage()
22 | }
23 |
24 | private func convertMarkdownText(of text: String) -> NSAttributedString {
25 | let markdown = SwiftyMarkdown(string: text)
26 | return markdown.attributedString()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueDetail/Models/IssueDetailEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailEndPoint.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | enum IssueDetailEndPoint: APIConfiguration {
11 | case getComments(Int)
12 | case postComment(Data)
13 |
14 | var method: HTTPMethod {
15 | switch self {
16 | case .getComments:
17 | return .get
18 | case .postComment:
19 | return .post
20 | }
21 | }
22 |
23 | var path: String {
24 | switch self {
25 | case .getComments(let id):
26 | return "/comment/\(id)"
27 | case .postComment:
28 | return "/comment"
29 | }
30 | }
31 |
32 | var body: Data? {
33 | switch self {
34 | case .getComments:
35 | return nil
36 | case .postComment(let data):
37 | return data
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FE/src/component/labelListPage/element/labelList.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import Label from "@component/labelListPage/element/label";
5 |
6 | const StyledLabelList = styled.div`
7 | display: flex;
8 | width: 100%;
9 | border: 1px solid gray;
10 | margin: 5px;
11 | flex-direction: column;
12 | `;
13 | const StyledLabelHeader = styled.div`
14 | display: flex;
15 | border: 1px solid gray;
16 | margin: 5px;
17 | `;
18 |
19 | function LabelList(props) {
20 | const { labels, setLabels } = props;
21 | return (
22 |
23 | Label List header
24 | {labels?.map((label, index) => (
25 |
26 | ))}
27 |
28 | );
29 | }
30 |
31 | export default hot(module)(LabelList);
32 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/PropertyWrapper/UserDefault.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefault.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | @propertyWrapper
11 | struct UserDefault {
12 | private let key: String
13 | private let defaultValue: String
14 |
15 | init(key: String, defaultValue: String = "") {
16 | self.key = key
17 | self.defaultValue = defaultValue
18 | }
19 |
20 | var wrappedValue: String {
21 | get {
22 | if let data = UserDefaults.standard.object(forKey: key) as? Data,
23 | let cart = try? JSONDecoder().decode(String.self, from: data) {
24 | return cart
25 | }
26 | return defaultValue
27 | }
28 | set {
29 | if let encoded = try? JSONEncoder().encode(newValue) {
30 | UserDefaults.standard.set(encoded, forKey: key)
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/BE/src/models/event.ts:
--------------------------------------------------------------------------------
1 | import db from "@providers/database";
2 | import Model from "@models/model";
3 | import { Event } from "@interfaces/event";
4 | import HTTPCODE from "@utils/magicnumber";
5 |
6 | class EventModel extends Model {
7 | protected tableName: string;
8 |
9 | constructor() {
10 | super();
11 | this.tableName = "EVENT";
12 | }
13 |
14 | async select(pData: T): Promise> {
15 | try {
16 | const result = await db.query(`SELECT * FROM ${this.tableName} WHERE issue_id = ?`, pData);
17 | this.data = [...result[0]];
18 | return this.data;
19 | } catch (err) {
20 | console.error(err);
21 | throw err;
22 | }
23 | }
24 |
25 | async add(pData: Event): Promise {
26 | try {
27 | this.data = await super.insert(pData, this.tableName);
28 | return this.data ? HTTPCODE.SUCCESS : HTTPCODE.FAIL;
29 | } catch {
30 | return HTTPCODE.SERVER_ERR;
31 | }
32 | }
33 | }
34 |
35 | export default new EventModel();
36 |
--------------------------------------------------------------------------------
/FE/README.md:
--------------------------------------------------------------------------------
1 | # IssueTracker iOS app
2 | [](https://github.com/boostcamp-2020/IssueTracker-3/releases)
3 | [](https://github.com/boostcamp-2020/IssueTracker-3/actions)
4 |
5 |
6 | ## Library
7 | 1. Alamofire
8 |
9 | ## Requirements
10 |
11 | - iOS 13.6+
12 | - Xcode 12.1+
13 | - Swift 5.3+
14 |
15 | ## Cocoapods
16 |
17 | ```ruby
18 | target 'IssueTracker' do
19 |
20 | pod 'SwiftLint'
21 |
22 | end
23 | ```
24 |
25 | ## Swift Package Manager
26 |
27 | ```swift
28 | import Alamofire
29 | ```
30 |
31 | ## Installation
32 |
33 | ```
34 | $ pod install
35 | ```
36 |
37 |
38 | ## Author
39 |
40 | - []()
41 | - []()
42 |
43 |
44 | ## License
45 |
46 | This code is distributed under the terms and conditions of the [MIT license](LICENSE).
47 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker.xcodeproj/xcuserdata/jaehyun.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | IssueTracker.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | IssueTrackerTests.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 1
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 708EFEE92546DBC9009D9DEC
21 |
22 | primary
23 |
24 |
25 | 708EFEFF2546DBCB009D9DEC
26 |
27 | primary
28 |
29 |
30 | 708EFF0A2546DBCB009D9DEC
31 |
32 | primary
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/element/userDropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Select, MenuItem } from "@material-ui/core";
5 |
6 | const Styled = styled.div`
7 | margin-right: 20px;
8 | `;
9 |
10 | function UserDropdown(props) {
11 | const { users, setCondition, condition } = props;
12 | const userOption = users.map((user) => {
13 | return { value: user.id, label: user.login_id };
14 | });
15 | userOption.unshift({ label: "user", value: "default" });
16 | const onUserHandler = (event) => {
17 | setCondition({ label: condition.label, milestone: condition.milestone, user: event.target.value });
18 | };
19 | return (
20 |
21 |
26 |
27 | );
28 | }
29 |
30 | export default hot(module)(UserDropdown);
31 |
--------------------------------------------------------------------------------
/FE/src/component/loginPage/buttons/loginSubmitButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 | import axiosApi from "@util/axiosApi";
6 |
7 | const StyledSubmitButton = styled.button`
8 | display: flex;
9 | border: 1px dotted black;
10 | margin: 5px;
11 | `;
12 | function SubmitButton(props) {
13 | const onSubmitAction = async () => {
14 | const data = { user_id: props.inputData.name, password: props.inputData.password };
15 | const res = await axiosApi("/auth/login", "POST", data);
16 | if (res.data.state === "success") {
17 | props.setUser({ id: 1, name: data.user_id, url: "test" });
18 | localStorage.setItem("token", res.data.JWT);
19 | } else {
20 | alert("아이디 비밀번호를 확인해주세요");
21 | }
22 | };
23 | return (
24 |
25 | 로그인 하기
26 |
27 | );
28 | }
29 | export default hot(module)(SubmitButton);
30 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/SignIn/Models/SignInEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIRouter.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/10/28.
6 | //
7 |
8 | import Foundation
9 |
10 | enum SignInEndPoint: APIConfiguration {
11 | case signIn(Data)
12 | case signUp(Data)
13 | case apple(Data)
14 | case github
15 | case token(Data)
16 |
17 | var method: HTTPMethod {
18 | return .post
19 | }
20 |
21 | var path: String {
22 | switch self {
23 | case .signIn: return "/auth/login"
24 | case .signUp: return "/auth/register"
25 | case .apple: return "/auth/apple"
26 | case .github: return "/auth/github/callback"
27 | case .token: return "/auth/github/token"
28 | }
29 | }
30 |
31 | var body: Data? {
32 | switch self {
33 | case .signIn(let data): return data
34 | case .signUp(let data): return data
35 | case .apple(let data): return data
36 | case .github: return nil
37 | case .token(let data): return data
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueFilter/Views/HeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeaderView.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/02.
6 | //
7 |
8 | import UIKit
9 |
10 | final class HeaderView: UIView {
11 |
12 | init(label: UILabel) {
13 | super.init(frame: .zero)
14 |
15 | self.addSubview(label)
16 |
17 | NSLayoutConstraint.activate([
18 | label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 5),
19 | label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16)
20 | ])
21 | }
22 |
23 | convenience init(text: String) {
24 | let uiLabel = UILabel().makeLabel(text: text)
25 | uiLabel.translatesAutoresizingMaskIntoConstraints = false
26 | self.init(label: uiLabel)
27 | }
28 |
29 | required init?(coder: NSCoder) {
30 | super.init(coder: coder)
31 | }
32 | }
33 |
34 | extension UILabel {
35 | func makeLabel(text: String) -> UILabel {
36 | let uiLabel = UILabel()
37 | uiLabel.text = text
38 | return uiLabel
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/element/labelDropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Select, MenuItem } from "@material-ui/core";
5 |
6 | const Styled = styled.div`
7 | margin-right: 20px;
8 | `;
9 | function LabelDropdown(props) {
10 | const { labels, setCondition, condition } = props;
11 | const labelOption = labels.map((label) => {
12 | return { value: label.name, label: label.name };
13 | });
14 | labelOption.unshift({ label: "issue with no label", value: "empty" });
15 | labelOption.unshift({ label: "label", value: "default" });
16 | const onLabelHandler = (event) => {
17 | setCondition({ label: event.target.value, milestone: condition.milestone, user: condition.user });
18 | };
19 | return (
20 |
21 |
26 |
27 | );
28 | }
29 |
30 | export default hot(module)(LabelDropdown);
31 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueDetail/IssueDetailBottomSheetViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailBottomSheetViewController.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/03.
6 | //
7 |
8 | import UIKit
9 |
10 | class IssueDetailBottomSheetViewController: UIViewController {
11 |
12 | @IBOutlet weak var handleArea: UIView!
13 | @IBOutlet weak var addCommentButton: UIButton!
14 | @IBOutlet weak var upButtom: UIButton!
15 | @IBOutlet weak var downButton: UIButton!
16 |
17 | weak var delegate: IssueDetailBottomSheetDelegate?
18 | var issueID: Int?
19 |
20 | @IBAction func addCommentTouched(_ sender: Any) {
21 | delegate?.addCommentViewShouldAppear()
22 | }
23 |
24 | @IBAction func scrollUpTouched(_ sender: Any) {
25 | delegate?.issueDetailViewShouldScrollUp()
26 | }
27 |
28 | @IBAction func scrollDownTouched(_ sender: Any) {
29 | delegate?.issueDetailViewShouldScrollDown()
30 | }
31 |
32 | @IBAction func closeIssueTouched(_ sender: Any) {
33 | delegate?.issueDetailViewShouldCloseIssue()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 IssueTracker-3
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/buttons/moveToMilestoneButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 | import useRequest from "../../../util/useRequest";
6 |
7 | const StyledMoveToMilestoneButton = styled.button`
8 | display: flex;
9 | border: 1px solid gray;
10 | background-color: white;
11 | font-weight: bold;
12 | margin: 5px 0px;
13 | border-top-right-radius: 4px;
14 | border-bottom-right-radius: 4px;
15 |
16 | &:hover {
17 | background-color: aquamarine;
18 | }
19 | `;
20 | const StyledNumber = styled.span`
21 | border: 1px solid gray;
22 | border-radius: 1px;
23 | background-color: gray;
24 | font-weight: bold;
25 | margin: 0px 3px;
26 | `;
27 | function MoveToMilestoneButton(props) {
28 | const { milestones } = props;
29 | return (
30 |
31 |
32 | 마일스톤{milestones.length}
33 |
34 |
35 | );
36 | }
37 |
38 | export default hot(module)(MoveToMilestoneButton);
39 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/buttons/moveToLabelButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 | import useRequest from "../../../util/useRequest";
6 |
7 | const StyledMoveToLabelButton = styled.button`
8 | display: flex;
9 | border: 1px solid gray;
10 | background-color: white;
11 | font-weight: bold;
12 | margin: 5px 0px;
13 | border-top-left-radius: 4px;
14 | border-bottom-left-radius: 4px;
15 |
16 | &:hover {
17 | background-color: aquamarine;
18 | }
19 | `;
20 | const StyledNumber = styled.span`
21 | border: 1px solid gray;
22 | border-radius: 1px;
23 | background-color: gray;
24 | font-weight: bold;
25 | margin: 0px 3px;
26 | `;
27 | function MoveToLabelButton(props) {
28 | const { labels } = props;
29 |
30 | return (
31 |
32 |
33 | 라벨{labels.length}
34 |
35 |
36 | );
37 | }
38 |
39 | export default hot(module)(MoveToLabelButton);
40 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/element/milestoneDropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Select, MenuItem } from "@material-ui/core";
5 |
6 | const Styled = styled.div`
7 | margin-right: 20px;
8 | `;
9 |
10 | function MilestoneDropdown(props) {
11 | const { milestones, setCondition, condition } = props;
12 | const milestoneOption = milestones.map((milestone) => {
13 | return { value: milestone.name, label: milestone.name };
14 | });
15 | milestoneOption.unshift({ label: "issue with no milestone", value: "empty" });
16 | milestoneOption.unshift({ label: "milestone", value: "default" });
17 | const onMilestoneHandler = (event) => {
18 | setCondition({ label: condition.label, milestone: event.target.value, user: condition.user });
19 | };
20 | return (
21 |
22 |
27 |
28 | );
29 | }
30 |
31 | export default hot(module)(MilestoneDropdown);
32 |
--------------------------------------------------------------------------------
/BE/src/models/user.ts:
--------------------------------------------------------------------------------
1 | import Model from "@models/model";
2 | import db from "@providers/database";
3 | import { User } from "@interfaces/user";
4 |
5 | class UserModel extends Model {
6 | protected tableName: string;
7 |
8 | constructor() {
9 | super();
10 | this.tableName = "USER";
11 | }
12 |
13 | async select(pData: T): Promise {
14 | try {
15 | const result = await db.query(`SELECT * FROM USER WHERE login_id = ? AND password = ?`, pData);
16 | this.data = [...result[0]];
17 | return this.data[0].id;
18 | } catch (err) {
19 | console.error(err);
20 | throw err;
21 | }
22 | }
23 |
24 | async selectAll(): Promise {
25 | try {
26 | const result = await db.query("SELECT * FROM USER");
27 | this.data = [...result[0]];
28 | return this.data;
29 | } catch (err) {
30 | console.error(err);
31 | throw err;
32 | }
33 | }
34 |
35 | async add(pData: User): Promise {
36 | try {
37 | this.data = await super.insert(pData, this.tableName);
38 | return this.data;
39 | } catch (err) {
40 | console.error(err);
41 | throw err;
42 | }
43 | }
44 | }
45 |
46 | const user = new UserModel();
47 | export default user;
48 |
--------------------------------------------------------------------------------
/FE/src/component/issueListPage/element/checkFilter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 | import { Select, MenuItem } from "@material-ui/core";
5 | import axiosApi from "../../../util/axiosApi";
6 |
7 | const Styled = styled.div`
8 | display: flex;
9 | width: 100%;
10 | justify-content: flex-end;
11 | .right {
12 | margin-left: auto;
13 | }
14 | .item {
15 | margin-left: 20px;
16 | }
17 | `;
18 |
19 | function CheckFilter(props) {
20 | const { checked } = props;
21 | const onCheckedHandler = async (event) => {
22 | for (const issue of checked) {
23 | await axiosApi(`/issue/${issue}/state/${event.target.value}`, "PATCH");
24 | }
25 | window.location.href = "/";
26 | };
27 | return (
28 |
29 | {checked.length} item selected
30 |
37 |
38 | );
39 | }
40 |
41 | export default hot(module)(CheckFilter);
42 |
--------------------------------------------------------------------------------
/FE/src/component/milestonePage/buttonWrapper.jsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import React, { useEffect, useState } from "react";
3 | import { hot } from "react-hot-loader";
4 | import styled from "styled-components";
5 | import MoveToLabelButton from "../labelListPage/buttons/moveToLabelButton";
6 | import MoveToMaileStoneButton from "../labelListPage/buttons/moveToMilestoneButton";
7 | import NewMilestoneButton from "./buttons/newButton";
8 |
9 | const DivButtonWrapper = styled.div`
10 | display: flex;
11 | width: 100%;
12 | height: 50px;
13 | border: 1px dotted black;
14 | box-sizing: border-box;
15 | justify-content: space-between;
16 | `;
17 |
18 | const ButtonColumn = styled.div`
19 | height: 100%;
20 | border: 1px dotted black;
21 | box-sizing: border-box;
22 | display: flex;
23 | align-items: center;
24 | `;
25 | function MilestoneListHeader() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | export default hot(module)(MilestoneListHeader);
40 |
--------------------------------------------------------------------------------
/BE/src/controllers/user.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { User } from "@interfaces/user";
3 | import UserModel from "@models/user";
4 |
5 | const add = async (req: Request, res: Response): Promise => {
6 | const user: User = {
7 | id: null,
8 | login_id: req.body.user_id,
9 | password: req.body.password,
10 | img: req.body?.img ?? "https://user-images.githubusercontent.com/5876149/97951341-39d26600-1ddd-11eb-94e7-9102b90bda8b.jpg",
11 | created_at: new Date(),
12 | };
13 | try {
14 | await UserModel.add(user);
15 | return res.status(201).json({ status: "success" });
16 | } catch {
17 | return res.status(400).json({ status: "fail" });
18 | }
19 | };
20 |
21 | const find = async (userID: string, password: string): Promise => {
22 | const rawPassword = password;
23 | const encrpytPassword = rawPassword;
24 | try {
25 | const result = await UserModel.select([userID, encrpytPassword]);
26 | return result;
27 | } catch (err) {
28 | return false;
29 | }
30 | };
31 | const getAll = async (): Promise => {
32 | try {
33 | const result = await UserModel.selectAll();
34 | return result;
35 | } catch (err) {
36 | return false;
37 | }
38 | };
39 | export default { find, add, getAll };
40 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Entity/Comment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comment.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias DetailCommentList = [DetailComment]
11 |
12 | struct DetailComment: Codable {
13 | let id: Int
14 | let body: String
15 | let createdAt: String
16 | let emoji: String
17 | let loginID: String
18 | let img: String
19 |
20 | enum CodingKeys: String, CodingKey {
21 | case id, body
22 | case createdAt = "created_at"
23 | case emoji
24 | case loginID = "login_id"
25 | case img
26 | }
27 | }
28 |
29 | typealias CommentList = [Comment]
30 |
31 | struct Comment: Codable {
32 | let id: Int
33 | let userID: Int?
34 | let body: String
35 | let emoji: String
36 | let issueID: Int?
37 | let createdAt: String
38 | let title: String?
39 | let state: Int?
40 | let milestoneID: MilestoneID?
41 | let closedAt: String?
42 |
43 | enum CodingKeys: String, CodingKey {
44 | case id, body, emoji, title, state
45 | case userID = "user_id"
46 | case createdAt = "created_at"
47 | case issueID = "issue_id"
48 | case milestoneID = "milestone_id"
49 | case closedAt = "closed_at"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Label/LabelInteractor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelInteractor.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LabelDataStore {
11 | var labels: LabelList { get set }
12 | }
13 |
14 | protocol LabelBusinessLogic {
15 | func fetchLabels()
16 | }
17 |
18 | final class LabelInteractor: LabelDataStore {
19 | let networkService: NetworkServiceProvider = NetworkService()
20 | var presenter: LabelPresentationLogic?
21 | var labels = LabelList()
22 | }
23 |
24 | extension LabelInteractor: LabelBusinessLogic {
25 | func fetchLabels() {
26 | networkService.request(apiConfiguration: LabelEndPoint.getLebels) { [weak self] result in
27 | guard let self = self else { return }
28 | switch result {
29 | case .failure(let error):
30 | debugPrint(error)
31 | return
32 | case .success(let data):
33 | guard let decodedData: LabelList = try? data.decoded() else {
34 | debugPrint("decode 실패")
35 | return
36 | }
37 | self.labels = decodedData
38 | self.presenter?.presentFetchedLabels(labels: self.labels)
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/FE/src/component/labelListPage/element/label.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { hot } from "react-hot-loader";
3 | import styled from "styled-components";
4 |
5 | import axiosApi from "../../../util/axiosApi";
6 |
7 | const StyledLabel = styled.div`
8 | display: flex;
9 | border: 0px solid gray;
10 | margin: 4px;
11 | `;
12 | function Label(props) {
13 | const { labelid, name, color, description, setLabels } = props;
14 | const Combination = styled.span`
15 | width: 25%;
16 | background-color: ${color || "RED"};
17 | `;
18 | const Separate = styled.span`
19 | width: 25%;
20 | `;
21 | const editLabel = async (event) => {
22 | const response = await axiosApi("/label", "GET");
23 | setLabels(response.data);
24 | };
25 | const deleteLabel = async (event) => {
26 | await axiosApi("/label", "DELETE", { id: labelid });
27 | const response = await axiosApi("/label", "GET");
28 | setLabels(response.data);
29 | };
30 | return (
31 |
32 | {name}
33 | {description}
34 |
35 |
36 | edit
37 | delete
38 |
39 |
40 | );
41 | }
42 |
43 | export default hot(module)(Label);
44 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Common/Extension/UIColor+hex.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+hex.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/04.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 | /// hex값으로 UIColor으로 초기화
12 | ///
13 | /// hex값을 입력하면 해당하는 색을 UIColor로 생성해준다.
14 | /// ```
15 | /// let color = UIColor(hex: "#000000")
16 | ///
17 | /// ```
18 | /// - Parameter hex: alpha값을 제외한 hex값
19 | convenience init?(hex: String) {
20 | let r, g, b, a: CGFloat
21 |
22 | if hex.hasPrefix("#") {
23 | let start = hex.index(hex.startIndex, offsetBy: 1)
24 | let hexColor = String(hex[start...])
25 |
26 | if hexColor.count == 6 {
27 | let scanner = Scanner(string: hexColor)
28 | var hexNumber: UInt64 = 0
29 |
30 | if scanner.scanHexInt64(&hexNumber) {
31 | r = CGFloat((hexNumber & 0xff0000) >> 16) / 255
32 | g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255
33 | b = CGFloat(hexNumber & 0x0000ff) / 255
34 | a = 1.0
35 |
36 | self.init(red: r, green: g, blue: b, alpha: a)
37 | return
38 | }
39 | }
40 | }
41 | return nil
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Milestone/Models/MilestoneViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MilestoneViewModel.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | class MilestoneViewModel: Hashable {
11 | let id: Int
12 | let milestoneButton: CustomButtonView
13 | let description: String
14 | let dueDate: String
15 | let percentage: String
16 | let openIssuesCount: String
17 | let closedIssuesCount: String
18 | let identifier = UUID()
19 |
20 | init(milestone: Milestone) {
21 | self.id = milestone.id
22 | self.milestoneButton = CustomButtonView(type: .milestone, text: milestone.name, color: "#ffffff")
23 | self.description = milestone.description
24 | self.dueDate = milestone.dueDate
25 | self.percentage = String(MilestoneCalculator.percentage(of: milestone.id)) + " %"
26 | self.openIssuesCount = String(MilestoneCalculator[milestone.id, .open]) + " open"
27 | self.closedIssuesCount = String(MilestoneCalculator[milestone.id, .closed]) + " closed"
28 | }
29 |
30 | func hash(into hasher: inout Hasher) {
31 | hasher.combine(identifier)
32 | }
33 |
34 | static func == (lhs: MilestoneViewModel, rhs: MilestoneViewModel) -> Bool {
35 | return lhs.identifier == rhs.identifier
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Label/Models/LabelEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelEndPoint.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LabelEndPoint: APIConfiguration {
11 | case getLebels
12 | case addLabel(Data)
13 | case editLabel(Data)
14 | case deleteLabel(Data)
15 |
16 | var method: HTTPMethod {
17 | switch self {
18 | case .getLebels:
19 | return .get
20 | case .addLabel:
21 | return .post
22 | case .editLabel:
23 | return .patch
24 | case .deleteLabel:
25 | return .delete
26 | }
27 | }
28 |
29 | var path: String {
30 | switch self {
31 | case .getLebels:
32 | return "/label"
33 | case .addLabel:
34 | return "/label"
35 | case .editLabel:
36 | return "/label"
37 | case .deleteLabel:
38 | return "/label"
39 | }
40 | }
41 |
42 | var body: Data? {
43 | switch self {
44 | case .getLebels:
45 | return nil
46 | case .addLabel(let data):
47 | return data
48 | case .editLabel(let data):
49 | return data
50 | case .deleteLabel(let data):
51 | return data
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/BE/src/models/tag.ts:
--------------------------------------------------------------------------------
1 | import db from "@providers/database";
2 | import { Tag } from "@interfaces/tag";
3 | import Model from "@models/model";
4 | import HTTPCODE from "@utils/magicnumber";
5 |
6 | class TagModel extends Model {
7 | protected tableName: string;
8 |
9 | constructor() {
10 | super();
11 | this.tableName = "TAG";
12 | }
13 |
14 | async select(pId: T): Promise> {
15 | try {
16 | const result = await db.query(
17 | `
18 | select t.id, l.name, l.color
19 | from ${this.tableName} t
20 | join LABEL l on t.label_id = l.id
21 | where t.issue_id = ?`,
22 | pId
23 | );
24 | this.data = [...result[0]];
25 | return this.data;
26 | } catch (err) {
27 | console.error(err);
28 | throw err;
29 | }
30 | }
31 |
32 | async add(pData: Tag): Promise {
33 | try {
34 | this.data = await super.insert(pData, this.tableName);
35 | return this.data ? this.data : HTTPCODE.FAIL;
36 | } catch {
37 | return HTTPCODE.SERVER_ERR;
38 | }
39 | }
40 |
41 | async del(id: number): Promise {
42 | try {
43 | this.data = await super.delete(id, this.tableName);
44 | return this.data ? HTTPCODE.SUCCESS : HTTPCODE.FAIL;
45 | } catch {
46 | return HTTPCODE.SERVER_ERR;
47 | }
48 | }
49 | }
50 |
51 | export default new TagModel();
52 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/CreateIssue/Models/CreateIssueEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateIssueEndPoint.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | enum CreateIssueEndPoint: APIConfiguration {
11 | case upload(Data)
12 | case edit(Data)
13 | case assignee(Data, Int)
14 | case label(Data, Int)
15 |
16 | var method: HTTPMethod {
17 | switch self {
18 | case .upload:
19 | return .post
20 | case .edit:
21 | return .patch
22 | case .assignee:
23 | return .patch
24 | case .label:
25 | return .patch
26 | }
27 | }
28 |
29 | var path: String {
30 | switch self {
31 | case .upload:
32 | return "/issue"
33 | case .edit:
34 | return "/issue"
35 | case .assignee(_, let id):
36 | return "/assignee/\(id)"
37 | case .label(_, let id):
38 | return "/tag/\(id)"
39 | }
40 | }
41 |
42 | var body: Data? {
43 | switch self {
44 | case .upload(let data):
45 | return data
46 | case .edit(let data):
47 | return data
48 | case .assignee(let data, _):
49 | return data
50 | case .label(let data, _):
51 | return data
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Milestone/MilestoneInterator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MilestoneInterator.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol MilestoneDataStore {
11 | var milestones: MilestoneList { get set }
12 | }
13 |
14 | protocol MilestoneBusinessLogic {
15 | func fetchMilestones()
16 | }
17 |
18 | final class MilestoneInteractor: MilestoneDataStore {
19 | let networkService: NetworkServiceProvider = NetworkService()
20 | var presenter: MilestonePresentationLogic?
21 | var milestones = MilestoneList()
22 | }
23 |
24 | extension MilestoneInteractor: MilestoneBusinessLogic {
25 | func fetchMilestones() {
26 | networkService.request(apiConfiguration: MilestoneEndPoint.getMilestones) { [weak self] result in
27 | guard let self = self else { return }
28 | switch result {
29 | case .failure(let error):
30 | debugPrint(error)
31 | return
32 | case .success(let data):
33 | guard let decodedData: MilestoneList = try? data.decoded() else {
34 | debugPrint("decode 실패")
35 | return
36 | }
37 | self.milestones = decodedData
38 | self.presenter?.presentFetchedMilestones(milestones: self.milestones)
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/BE/src/controllers/label.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import LabelModel from "@models/label";
3 | import { Label } from "@interfaces/label";
4 | import HTTPCODE from "@utils/magicnumber";
5 |
6 | const get = async (req: Request, res: Response): Promise => {
7 | try {
8 | const result = await LabelModel.select();
9 | return res.json(result);
10 | } catch {
11 | return res.sendStatus(HTTPCODE.SERVER_ERR);
12 | }
13 | };
14 |
15 | const add = async (req: Request, res: Response): Promise => {
16 | const label: Label = {
17 | id: null,
18 | name: req.body.name,
19 | description: req.body?.description ?? null,
20 | color: req.body.color,
21 | created_at: new Date(),
22 | };
23 | const result = await LabelModel.add(label);
24 | return res.status(result.httpcode).json(result.message);
25 | };
26 | const edit = async (req: Request, res: Response): Promise => {
27 | const label = {
28 | id: req.body.id,
29 | name: req.body.name,
30 | description: req.body?.description ?? null,
31 | color: req.body.color,
32 | };
33 | const result = await LabelModel.edit(label);
34 | return res.sendStatus(result);
35 | };
36 |
37 | const del = async (req: Request, res: Response): Promise => {
38 | const { id } = req.body;
39 | const result = await LabelModel.del(id);
40 | return res.sendStatus(result);
41 | };
42 |
43 | export default { get, add, edit, del };
44 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Milestone/Models/MilestoneEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MilestoneEndPoint.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | enum MilestoneEndPoint: APIConfiguration {
11 | case getMilestones
12 | case addMilestone(Data)
13 | case editMilestone(Data)
14 | case deleteMilestone(Data)
15 |
16 | var method: HTTPMethod {
17 | switch self {
18 | case .getMilestones:
19 | return .get
20 | case .addMilestone:
21 | return .post
22 | case .editMilestone:
23 | return .patch
24 | case .deleteMilestone:
25 | return .delete
26 | }
27 | }
28 |
29 | var path: String {
30 | switch self {
31 | case .getMilestones:
32 | return "/milestone"
33 | case .addMilestone:
34 | return "/milestone"
35 | case .editMilestone:
36 | return "/milestone"
37 | case .deleteMilestone:
38 | return "/milestone"
39 | }
40 | }
41 |
42 | var body: Data? {
43 | switch self {
44 | case .getMilestones:
45 | return nil
46 | case .addMilestone(let data):
47 | return data
48 | case .editMilestone(let data):
49 | return data
50 | case .deleteMilestone(let data):
51 | return data
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTrackerTests/IssueListTests/IssueListMakeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueListMakeTests.swift
3 | // IssueTrackerTests
4 | //
5 | // Created by ParkJaeHyun on 2020/11/03.
6 | //
7 |
8 | import XCTest
9 | @testable import IssueTracker
10 |
11 | class IssueListMakeTests: XCTestCase {
12 | func testExample() throws {
13 | // Given
14 | let issueMock = IssuesMock()
15 | let makedIssues = issueMock.issues
16 | let controller = IssueListModelController()
17 |
18 | let originCount = makedIssues.count
19 |
20 | let addModel = IssueListViewModel(
21 | id: 1,
22 | title: "haha",
23 | description: "설명",
24 | milestone: CustomButtonView(type: .milestone,
25 | text: "프로젝트",
26 | color: "#ffffff"),
27 | labels: [CustomButtonView(type: .label,
28 | text: "label",
29 | color: "#ffffff"),
30 | CustomButtonView(type: .label,
31 | text: "labe",
32 | color: "#ffffff")])
33 |
34 | // When
35 | let makedIssueListViewModel = controller.add(model: addModel, to: makedIssues)
36 |
37 | // Then
38 | XCTAssertEqual(makedIssueListViewModel.count, originCount + 1)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/Milestone/Views/MilestoneCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MilestoneCollectionViewCell.swift
3 | // IssueTracker
4 | //
5 | // Created by 송민관 on 2020/11/11.
6 | //
7 |
8 | import UIKit
9 |
10 | class MilestoneCollectionViewCell: UICollectionViewCell {
11 |
12 | @IBOutlet private weak var milestoneStackView: UIStackView!
13 | @IBOutlet private weak var descriptionLabel: UILabel!
14 | @IBOutlet private weak var dueDateLabel: UILabel!
15 | @IBOutlet private weak var percentageLabel: UILabel!
16 | @IBOutlet private weak var openIssuesCountLabel: UILabel!
17 | @IBOutlet private weak var closedIssuesCountLabel: UILabel!
18 |
19 | func configure(viewModel: MilestoneViewModel) {
20 | milestoneStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
21 | milestoneStackView.addArrangedSubview(viewModel.milestoneButton)
22 | descriptionLabel.text = viewModel.description
23 | dueDateLabel.text = convertDueDateFormat(of: viewModel.dueDate)
24 | percentageLabel.text = viewModel.percentage
25 | openIssuesCountLabel.text = viewModel.openIssuesCount
26 | closedIssuesCountLabel.text = viewModel.closedIssuesCount
27 | }
28 |
29 | private func convertDueDateFormat(of dueDate: String) -> String {
30 | guard let dueDate = dueDate.toDate() else {
31 | return ""
32 | }
33 | return DateFormatter.format(dueDate)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueDetail/Models/IssueDetailViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailViewModel.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | class IssueDetailViewModel: Hashable {
11 | let id: Int
12 | let body: String
13 | let emoji: String
14 | var createdAt: String
15 | let img: String
16 | let identifier = UUID()
17 |
18 | init(id: Int,
19 | body: String,
20 | emoji: String,
21 | createdAt: String,
22 | img: String) {
23 | self.id = id
24 | self.body = body
25 | self.emoji = emoji
26 | self.createdAt = createdAt
27 | self.img = img
28 | }
29 |
30 | init(commentList: DetailComment) {
31 | self.id = commentList.id
32 | self.body = commentList.body
33 | self.emoji = commentList.emoji
34 | self.createdAt = commentList.createdAt
35 | self.img = commentList.img
36 | self.setAgoTime(commentList.createdAt)
37 | }
38 |
39 | func setAgoTime(_ time: String) {
40 | guard let time = time.toDate()?.timeAgoDisplay() else { return }
41 | self.createdAt = time
42 | }
43 |
44 | func hash(into hasher: inout Hasher) {
45 | hasher.combine(identifier)
46 | }
47 |
48 | static func == (lhs: IssueDetailViewModel, rhs: IssueDetailViewModel) -> Bool {
49 | return lhs.identifier == rhs.identifier
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/FE/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require("html-webpack-plugin");
2 | const path = require("path");
3 |
4 | module.exports = {
5 | mode: "development",
6 | entry: {
7 | main: ["babel-polyfill", "./src/index.js"],
8 | },
9 | output: {
10 | filename: "bundle.[name].js",
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.(js|jsx)$/,
16 | exclude: /node_modules/,
17 | use: {
18 | loader: "babel-loader",
19 | },
20 | },
21 | {
22 | test: /\.html$/,
23 | use: [
24 | {
25 | loader: "html-loader",
26 | options: {
27 | minimize: true,
28 | },
29 | },
30 | ],
31 | },
32 | {
33 | test: /\.css$/,
34 | use: "css-loader",
35 | },
36 | ],
37 | },
38 | resolve: {
39 | extensions: [".js", ".jsx"],
40 | alias: {
41 | "@": path.resolve(__dirname, "src/"),
42 | "@component": path.resolve(__dirname, "src/component"),
43 | "@pages": path.resolve(__dirname, "src/pages"),
44 | "@util": path.resolve(__dirname, "src/util"),
45 | },
46 | },
47 | plugins: [
48 | new HtmlWebpackPlugin({
49 | template: "./public/index.html",
50 | }),
51 | ],
52 | devServer: {
53 | contentBase: path.join(__dirname, "dist"),
54 | compress: true,
55 | host: "localhost",
56 | port: 5000,
57 | open: true,
58 | historyApiFallback: true,
59 | },
60 | };
61 |
--------------------------------------------------------------------------------
/BE/src/models/model.ts:
--------------------------------------------------------------------------------
1 | import db from "@providers/database";
2 |
3 | export default abstract class Model {
4 | data: any;
5 |
6 | protected abstract tableName: string;
7 |
8 | constructor() {
9 | this.data = 0;
10 | }
11 |
12 | async insert(pData: T, pTableName: string): Promise {
13 | try {
14 | const data = await db.query(`INSERT INTO ${pTableName} SET ?`, pData);
15 | const { insertId } = data[0];
16 | this.data = insertId;
17 | return this.data;
18 | } catch (err) {
19 | console.error(err);
20 | throw err;
21 | }
22 | }
23 |
24 | async update(pData: T, pTableName: string): Promise {
25 | try {
26 | const id = Object.entries(pData)[0][1];
27 | const result = await db.query>(`UPDATE ${pTableName} SET ? WHERE id = ?`, [pData, id]);
28 | const { affectedRows } = result[0];
29 | this.data = affectedRows;
30 | return !!this.data;
31 | } catch (err) {
32 | console.error(err);
33 | throw err;
34 | }
35 | }
36 |
37 | async delete(id: number, pTableName: string): Promise {
38 | try {
39 | const result = await db.query(`DELETE FROM ${pTableName} WHERE id = ?`, id);
40 | const { affectedRows } = result[0];
41 | this.data = affectedRows;
42 | return !!this.data;
43 | } catch (err) {
44 | console.error(err);
45 | throw err;
46 | }
47 | }
48 |
49 | abstract async select(pData: T): Promise;
50 | }
51 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueFilter/Models/IssueFilterViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueListViewModel.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/04.
6 | //
7 |
8 | import UIKit
9 |
10 | final class IssueFilterViewModel: Hashable {
11 | let title: Filter
12 | let chevronDirection: [ChevronDirection]
13 | let childTitle: String = ""
14 | let image: UIImage? = nil
15 | let childItem: [IssueFilterViewModel]
16 | let identifier = UUID()
17 | var isChevron: Bool
18 | var hasChildren: Bool
19 | var needsSeparator: Bool = true
20 |
21 | init(title: Filter,
22 | chevronDirection: [ChevronDirection],
23 | isChevron: Bool,
24 | hasChildren: Bool = false,
25 | childItem: [IssueFilterViewModel] = [],
26 | childTitle: String = "",
27 | image: UIImage? = nil) {
28 | self.title = title
29 | self.hasChildren = hasChildren
30 | self.childItem = childItem
31 | self.isChevron = isChevron
32 |
33 | for child in self.childItem {
34 | child.needsSeparator = false
35 | }
36 |
37 | self.childItem.last?.needsSeparator = true
38 | self.chevronDirection = chevronDirection
39 | }
40 |
41 | func hash(into hasher: inout Hasher) {
42 | hasher.combine(identifier)
43 | }
44 |
45 | static func == (lhs: IssueFilterViewModel, rhs: IssueFilterViewModel) -> Bool {
46 | return lhs.identifier == rhs.identifier
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/BE/src/controllers/comment.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import CommentModel from "@models/comment";
3 | import { Comment } from "@interfaces/comment";
4 | import HTTPCODE from "@utils/magicnumber";
5 |
6 | const get = async (req: Request, res: Response): Promise => {
7 | try {
8 | const result = await CommentModel.select(+req.params.issueid);
9 | return res.json(result);
10 | } catch {
11 | return res.sendStatus(HTTPCODE.SERVER_ERR);
12 | }
13 | };
14 |
15 | const add = async (req: Request, res: Response): Promise => {
16 | const comment: Comment = {
17 | id: null,
18 | issue_id: req.body.issue_id,
19 | user_id: req.body.user_id,
20 | body: req.body.body,
21 | emoji: req.body?.emoji ?? null,
22 | created_at: new Date(),
23 | };
24 | const result = await CommentModel.add(comment);
25 | return res.status(result.httpcode).json(result.message);
26 | };
27 |
28 | const edit = async (req: Request, res: Response): Promise => {
29 | const comment = {
30 | id: req.body.id,
31 | issue_id: req.body.issue_id,
32 | user_id: req.body.user_id,
33 | body: req.body.body,
34 | emoji: req.body.emoji,
35 | };
36 | const result = await CommentModel.edit(comment);
37 | return res.sendStatus(result);
38 | };
39 |
40 | const del = async (req: Request, res: Response): Promise => {
41 | const result = await CommentModel.del(+req.body.id);
42 | return res.sendStatus(result);
43 | };
44 |
45 | export default { get, add, edit, del };
46 |
--------------------------------------------------------------------------------
/BE/src/models/assignee.ts:
--------------------------------------------------------------------------------
1 | import db from "@providers/database";
2 | import Model from "@models/model";
3 | import { Assignee } from "@interfaces/assignee";
4 | import HTTPCODE from "@utils/magicnumber";
5 | import response from "@utils/response";
6 | import { resMessage } from "@interfaces/response";
7 |
8 | class AssigneeModel extends Model {
9 | protected tableName: string;
10 |
11 | constructor() {
12 | super();
13 | this.tableName = "ASSIGNEE";
14 | }
15 |
16 | async select(pId: T): Promise> {
17 | try {
18 | const data = await db.query(
19 | `
20 | select a.id, u.login_id, u.img
21 | from ${this.tableName} a
22 | join USER u on a.user_id = u.id
23 | where a.issue_id = ?`,
24 | pId
25 | );
26 | this.data = [...data[0]];
27 | return this.data;
28 | } catch (err) {
29 | console.error(err);
30 | throw err;
31 | }
32 | }
33 |
34 | async add(pData: Assignee): Promise {
35 | try {
36 | this.data = await super.insert(pData, this.tableName);
37 | return this.data ? response(HTTPCODE.SUCCESS, `${this.data}`) : response(HTTPCODE.FAIL, `fail insert`);
38 | } catch {
39 | return response(HTTPCODE.SERVER_ERR, `internal server error`);
40 | }
41 | }
42 |
43 | async del(id: number): Promise {
44 | try {
45 | this.data = await super.delete(id, this.tableName);
46 | return this.data ? HTTPCODE.SUCCESS : HTTPCODE.FAIL;
47 | } catch {
48 | return HTTPCODE.SERVER_ERR;
49 | }
50 | }
51 | }
52 |
53 | export default new AssigneeModel();
54 |
--------------------------------------------------------------------------------
/BE/src/providers/passport.ts:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 | import passportLocal from "passport-local";
3 | import passportGithub from "passport-github";
4 | import dotenv from "dotenv";
5 | import path from "path";
6 |
7 | dotenv.config({ path: path.join(__dirname, "../../.env") });
8 |
9 | const LocalStrategy = passportLocal.Strategy;
10 | const GithubStrategy = passportGithub.Strategy;
11 | class Passport {
12 | public config = () => {
13 | // Local Strategy
14 | passport.use(
15 | new LocalStrategy(
16 | {
17 | usernameField: "user_id",
18 | passwordField: "password",
19 | },
20 | async (userId: string, passWord: string, done) => {
21 | const user = { userID: userId, password: passWord };
22 | return done(null, user, { message: "Logged In Successfully" });
23 | }
24 | )
25 | );
26 | // Github Strategy
27 | passport.use(
28 | new GithubStrategy(
29 | {
30 | clientID: process.env.GIT_ID as string,
31 | clientSecret: process.env.GIT_PASSWORD as string,
32 | callbackURL: process.env.GIT_CALLBACK as string,
33 | },
34 | async (accessToken, refreshToken, profile, done) => {
35 | const user = { profile, accessToken };
36 | return done(null, user, { message: "Logged In Successfully" });
37 | }
38 | )
39 | );
40 | passport.serializeUser((user, done) => {
41 | done(null, user);
42 | });
43 |
44 | passport.deserializeUser((user, done) => {
45 | done(null, user);
46 | });
47 | };
48 | }
49 |
50 | export default Passport;
51 |
--------------------------------------------------------------------------------
/BE/src/controllers/tag.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-await-in-loop */
2 | /* eslint-disable no-restricted-syntax */
3 | import { Request, Response } from "express";
4 | import TagModel from "@models/tag";
5 | import { Tag } from "@interfaces/tag";
6 | import HTTPCODE from "@utils/magicnumber";
7 |
8 | const get = async (req: Request, res: Response): Promise => {
9 | try {
10 | const result = await TagModel.select(+req.params.issueid);
11 | return res.json(result);
12 | } catch {
13 | return res.sendStatus(HTTPCODE.SERVER_ERR);
14 | }
15 | };
16 |
17 | const edit = async (req: Request, res: Response): Promise => {
18 | try {
19 | const data = await TagModel.select(+req.params.issueid);
20 | const ids = data.map((value) => Number(value.id));
21 | for (const id of ids) {
22 | const result = await TagModel.del(id);
23 | if (result === HTTPCODE.FAIL) return res.sendStatus(HTTPCODE.FAIL);
24 | if (result === HTTPCODE.SERVER_ERR) return res.sendStatus(HTTPCODE.SERVER_ERR);
25 | }
26 | const issueId = Number(req.params.issueid);
27 | const tags = req.body.tags.map((value: number) => {
28 | const tag: Tag = {
29 | id: null,
30 | issue_id: issueId,
31 | label_id: value,
32 | };
33 | return tag;
34 | });
35 |
36 | for (const tag of tags) {
37 | const result = await TagModel.add(tag);
38 | if (result === HTTPCODE.FAIL) return res.sendStatus(HTTPCODE.FAIL);
39 | if (result === HTTPCODE.SERVER_ERR) return res.sendStatus(HTTPCODE.SERVER_ERR);
40 | }
41 |
42 | return res.sendStatus(HTTPCODE.SUCCESS);
43 | } catch {
44 | return res.sendStatus(HTTPCODE.SERVER_ERR);
45 | }
46 | };
47 | export default { get, edit };
48 |
--------------------------------------------------------------------------------
/iOS/IssueTracker/IssueTracker/IssueDetail/Views/IssueDetailCollectionReusableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueDetailCollectionReusableView.swift
3 | // IssueTracker
4 | //
5 | // Created by ParkJaeHyun on 2020/11/10.
6 | //
7 | import UIKit
8 | import SwiftyMarkdown
9 |
10 | class IssueDetailCollectionReusableView: UICollectionReusableView {
11 | static let identifier = "IssueDetailCollectionReusableView"
12 |
13 | @IBOutlet weak var authorLabel: UILabel!
14 | @IBOutlet weak var authorImageView: UIImageView!
15 | @IBOutlet weak var bodyLabel: UILabel!
16 | @IBOutlet weak var stateButton: UIButton!
17 | @IBOutlet weak var issueNumberLabel: UILabel!
18 |
19 | override init(frame: CGRect) {
20 | super.init(frame: frame)
21 | }
22 |
23 | required init?(coder: NSCoder) {
24 | super.init(coder: coder)
25 | }
26 |
27 | func configure(item: IssueListViewModel) {
28 | authorLabel.text = item.title
29 | issueNumberLabel.text = "#\(item.id ?? 0)"
30 | bodyLabel.attributedText = convertMarkdownText(of: item.description)
31 | if item.isOpen {
32 | stateButton.setTitle("Open", for: .normal)
33 | stateButton.setTitleColor(.systemYellow, for: .normal)
34 | stateButton.backgroundColor = .systemGreen
35 | } else {
36 | stateButton.setTitle("Close", for: .normal)
37 | stateButton.setTitleColor(.systemYellow, for: .normal)
38 | stateButton.backgroundColor = .systemRed
39 | }
40 | }
41 |
42 | private func convertMarkdownText(of text: String) -> NSAttributedString {
43 | let markdown = SwiftyMarkdown(string: text)
44 | return markdown.attributedString()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/BE/src/models/label.ts:
--------------------------------------------------------------------------------
1 | import { Label } from "@interfaces/label";
2 | import Model from "@models/model";
3 | import db from "@providers/database";
4 | import HTTPCODE from "@utils/magicnumber";
5 | import filter from "@utils/filter";
6 | import makeResponse from "@utils/response";
7 |
8 | class LabelModel extends Model {
9 | protected tableName: string;
10 |
11 | constructor() {
12 | super();
13 | this.tableName = "LABEL";
14 | }
15 |
16 | async select(): Promise> {
17 | try {
18 | const data = await db.query(`select * from ${this.tableName}`);
19 | this.data = [...data[0].map(filter.nullFilter)];
20 | return this.data;
21 | } catch (err) {
22 | console.error(err);
23 | throw err;
24 | }
25 | }
26 |
27 | async add(pData: Label): Promise {
28 | try {
29 | this.data = await super.insert