├── 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 |
8 |
9 | 10 |
, 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 | [![Releases](https://img.shields.io/github/v/release/boostcamp-2020/IssueTracker-3)](https://github.com/boostcamp-2020/IssueTracker-3/releases) 3 | [![build](https://github.com/boostcamp-2020/IssueTracker-3/workflows/Node.js%20CI/badge.svg)](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 |
17 | 18 | 19 | 20 | 21 |
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 | 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 | [![Releases](https://img.shields.io/github/v/release/boostcamp-2020/IssueTracker-3)](https://github.com/boostcamp-2020/IssueTracker-3/releases) 3 | [![build](https://github.com/boostcamp-2020/IssueTracker-3/workflows/Auto%20Unit%20Test%20and%20Fail%20Comment/badge.svg)](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