├── .babelrc
├── .eslintrc
├── .github
└── ISSUE_TEMPLATE
│ └── feature_request.md
├── .gitignore
├── .prettierrc
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── public
└── images
│ ├── alarmModal
│ ├── already.svg
│ └── checked.svg
│ ├── common
│ ├── 404.svg
│ ├── alarm.svg
│ ├── github.svg
│ ├── joinProfile.svg
│ ├── mic_team.png
│ ├── modalDog.svg
│ └── update.svg
│ ├── footerMenu
│ ├── menu01.svg
│ ├── menu02.svg
│ └── menu03.svg
│ ├── header
│ ├── FilledHeart.svg
│ ├── alarm.svg
│ ├── back.svg
│ ├── heart.svg
│ ├── nonAlarm.svg
│ ├── search.svg
│ ├── searchBack.svg
│ └── setting.svg
│ ├── home
│ └── dog.svg
│ ├── icon
│ └── dog.svg
│ ├── login
│ ├── character_color.svg
│ ├── email.svg
│ ├── kakao.svg
│ └── naver.svg
│ ├── post
│ ├── comment.svg
│ ├── comment_delet.svg
│ ├── recoment.svg
│ └── upload.svg
│ ├── signup
│ ├── character.svg
│ ├── character_com.svg
│ ├── check.svg
│ ├── job01.svg
│ ├── job02.svg
│ ├── job03.svg
│ ├── logo.svg
│ └── profile.svg
│ ├── splashScreen
│ ├── dog.svg
│ ├── logo01.svg
│ └── logo2.svg
│ └── together
│ ├── btn_delete.svg
│ └── btn_write.svg
├── src
├── apis
│ ├── dummyData
│ │ └── index.tsx
│ └── index.tsx
├── components
│ ├── Alarm
│ │ ├── List
│ │ │ └── index.tsx
│ │ └── Title
│ │ │ └── index.tsx
│ ├── AlarmModal
│ │ ├── index.tsx
│ │ └── styles.tsx
│ ├── AuthLogin
│ │ └── index.tsx
│ ├── Common
│ │ ├── BigTitle
│ │ │ └── index.tsx
│ │ ├── CommentModal
│ │ │ └── index.tsx
│ │ ├── FlatBox
│ │ │ └── index.tsx
│ │ ├── FootButton
│ │ │ └── index.tsx
│ │ ├── FooterMenu
│ │ │ └── index.tsx
│ │ ├── HashWrap
│ │ │ └── index.tsx
│ │ ├── Header
│ │ │ ├── Back
│ │ │ │ └── index.tsx
│ │ │ ├── BackOptional
│ │ │ │ └── index.tsx
│ │ │ ├── EditBackOptional
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── IsModal
│ │ │ └── index.tsx
│ │ ├── JobButton
│ │ │ └── index.tsx
│ │ ├── PasswordField
│ │ │ └── index.tsx
│ │ ├── TextField
│ │ │ └── index.tsx
│ │ ├── TextFieldProfile
│ │ │ └── index.tsx
│ │ ├── Title
│ │ │ └── index.tsx
│ │ ├── wellseeError
│ │ │ └── index.tsx
│ │ └── wellseeErrorHome
│ │ │ └── index.tsx
│ ├── ConfirmModal
│ │ ├── index.tsx
│ │ └── styles.tsx
│ ├── DataForm
│ │ ├── index.tsx
│ │ └── style.tsx
│ ├── EditForm
│ │ └── index.tsx
│ ├── Home
│ │ ├── Header
│ │ │ └── index.tsx
│ │ ├── Main
│ │ │ └── index.tsx
│ │ └── StudySection
│ │ │ └── index.tsx
│ ├── LikePost
│ │ └── index.tsx
│ ├── Loading
│ │ └── index.tsx
│ ├── LogOutModal
│ │ ├── index.tsx
│ │ └── styles.tsx
│ ├── Modal
│ │ ├── index.tsx
│ │ └── styles.tsx
│ ├── MyPage
│ │ ├── Career
│ │ │ ├── index.tsx
│ │ │ └── style.tsx
│ │ ├── Portfolio
│ │ │ ├── index.tsx
│ │ │ └── style.tsx
│ │ ├── Profile
│ │ │ ├── index.tsx
│ │ │ └── style.tsx
│ │ └── School
│ │ │ ├── index.tsx
│ │ │ └── style.tsx
│ ├── PortFolioDeleteForm
│ │ └── index.tsx
│ ├── Post
│ │ ├── EditComment
│ │ │ └── index.tsx
│ │ └── PostFooter
│ │ │ └── index.tsx
│ ├── SignupDeleteForm
│ │ └── index.tsx
│ ├── SplashScreen
│ │ └── index.tsx
│ ├── SubmitModal
│ │ ├── index.tsx
│ │ └── styles.tsx
│ └── Together
│ │ ├── Header
│ │ ├── Search
│ │ │ ├── index.tsx
│ │ │ └── style.tsx
│ │ ├── index.tsx
│ │ └── style.tsx
│ │ ├── SearchBox
│ │ ├── index.tsx
│ │ └── style.tsx
│ │ ├── StudyBox
│ │ ├── index.tsx
│ │ └── style.tsx
│ │ ├── StudySection
│ │ └── index.tsx
│ │ ├── StudySectionOption
│ │ └── index.tsx
│ │ ├── StudySlider
│ │ ├── index.tsx
│ │ └── style.tsx
│ │ └── WriteButton
│ │ └── index.tsx
├── hooks
│ ├── useHandleOverflow.ts
│ └── useHeader.ts
├── lib
│ └── apiClient.ts
├── pages
│ ├── 404.tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── _error.tsx
│ ├── alarm
│ │ └── index.tsx
│ ├── class_join_list
│ │ └── [id].tsx
│ ├── failure
│ │ └── index.tsx
│ ├── home
│ │ └── index.tsx
│ ├── index.tsx
│ ├── mypage
│ │ └── index.tsx
│ ├── posts
│ │ ├── [id].tsx
│ │ └── comment
│ │ │ └── [id].tsx
│ ├── sign_in
│ │ ├── auth_start
│ │ │ └── index.tsx
│ │ ├── email_start
│ │ │ └── index.tsx
│ │ ├── find_password
│ │ │ └── index.tsx
│ │ └── reset_password
│ │ │ └── index.tsx
│ ├── sign_up
│ │ ├── completion
│ │ │ └── index.tsx
│ │ ├── experience
│ │ │ ├── index.tsx
│ │ │ ├── need_update
│ │ │ │ └── index.tsx
│ │ │ └── update.tsx
│ │ ├── index.tsx
│ │ ├── portfolio
│ │ │ ├── index.tsx
│ │ │ ├── need_update
│ │ │ │ └── index.tsx
│ │ │ └── update.tsx
│ │ ├── profile_start
│ │ │ └── index.tsx
│ │ ├── school
│ │ │ ├── index.tsx
│ │ │ ├── need_update
│ │ │ │ └── index.tsx
│ │ │ └── update.tsx
│ │ ├── self_introduction
│ │ │ ├── index.tsx
│ │ │ ├── need_update
│ │ │ │ └── index.tsx
│ │ │ └── update.tsx
│ │ └── something_job
│ │ │ └── index.tsx
│ ├── template
│ │ └── index.tsx
│ ├── together
│ │ ├── index.tsx
│ │ ├── search
│ │ │ └── index.tsx
│ │ ├── search_result
│ │ │ └── [id].tsx
│ │ └── write
│ │ │ └── index.tsx
│ └── token
│ │ └── index.tsx
├── reducers
│ ├── comments
│ │ └── index.tsx
│ ├── common
│ │ └── index.tsx
│ ├── home
│ │ └── index.tsx
│ ├── index.tsx
│ ├── mypage
│ │ └── index.tsx
│ ├── notifications
│ │ └── index.tsx
│ ├── posts
│ │ └── index.tsx
│ └── todos
│ │ └── index.tsx
├── sagas
│ ├── comments
│ │ └── index.tsx
│ ├── home
│ │ └── index.tsx
│ ├── index.tsx
│ ├── mypage
│ │ └── index.tsx
│ ├── notifications
│ │ └── index.tsx
│ └── posts
│ │ └── index.tsx
├── store
│ └── index.tsx
├── styles
│ ├── common.tsx
│ └── global-styles.tsx
└── types
│ └── index.ts
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "preset-react": {
7 | "runtime": "automatic",
8 | "importSource": "@emotion/react"
9 | }
10 | }
11 | ]
12 | ],
13 | "plugins": ["@emotion/babel-plugin"]
14 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true,
5 | "es6": true,
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 8
9 | }, // to enable features such as async/await
10 | "ignorePatterns": ["node_modules/*", ".next/*", ".out/*", "!.prettierrc.js"], // We don"t want to lint generated files nor node_modules, but we want to lint .prettierrc.js (ignored by default by eslint)
11 | "extends": ["eslint:recommended"],
12 | "overrides": [
13 | // This configuration will apply only to TypeScript files
14 | {
15 | "files": ["**/*.ts", "**/*.tsx"],
16 | "parser": "@typescript-eslint/parser",
17 | "settings": {
18 | "react": {
19 | "version": "detect"
20 | }
21 | },
22 | "env": {
23 | "browser": true,
24 | "node": true,
25 | "es6": true,
26 | },
27 | "extends": [
28 | "eslint:recommended",
29 | "plugin:@typescript-eslint/recommended", // TypeScript rules
30 | "plugin:react/recommended", // React rules
31 | "plugin:react-hooks/recommended", // React hooks rules
32 | "plugin:jsx-a11y/recommended", // Accessibility rules
33 | "plugin:prettier/recommended", // Prettier plugin
34 | ],
35 | "rules": {
36 | // We will use TypeScript"s types for component props instead
37 | "react/prop-types": "off",
38 |
39 | // No need to import React when using Next.js
40 | "react/react-in-jsx-scope": "off",
41 |
42 | // This rule is not compatible with Next.js"s components
43 | "jsx-a11y/anchor-is-valid": "off",
44 |
45 | "jsx-a11y/accessible-emoji": "off",
46 |
47 | // Why would you want unused vars?
48 | "@typescript-eslint/no-unused-vars": ["error"],
49 |
50 | // I suggest this setting for requiring return types on functions only where useful
51 | "@typescript-eslint/explicit-function-return-type": "off",
52 | "@typescript-eslint/explicit-module-boundary-types": "off",
53 |
54 | // Includes .prettierrc.js rules
55 | "prettier/prettier": ["error", {}, {
56 | "usePrettierrc": true
57 | }],
58 | },
59 | },
60 | ],
61 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[WSC-XXX]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 📄 이슈 내용
11 | - 이슈 내용 요약 설명
12 |
13 | ## 📝 상세 내용
14 | - 이슈 내용 구현 관련 상세 내용 작성
15 |
16 | ## ✔ 체크리스트
17 | - [ ] TODO A
18 | - [ ] TODO B
19 | - [ ] TODO C
20 |
21 | Add any other context or screenshots about the feature request here.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/react,node
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=react,node
4 |
5 | ### Node ###
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # TypeScript v1 declaration files
50 | typings/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Microbundle cache
62 | .rpt2_cache/
63 | .rts2_cache_cjs/
64 | .rts2_cache_es/
65 | .rts2_cache_umd/
66 |
67 | # Optional REPL history
68 | .node_repl_history
69 |
70 | # Output of 'npm pack'
71 | *.tgz
72 |
73 | # Yarn Integrity file
74 | .yarn-integrity
75 |
76 | # dotenv environment variables file
77 | .env*
78 |
79 | # parcel-bundler cache (https://parceljs.org/)
80 | .cache
81 |
82 | # Next.js build output
83 | .next
84 | /out/
85 |
86 | # Nuxt.js build / generate output
87 | .nuxt
88 | dist
89 |
90 | # Gatsby files
91 | .cache/
92 | # Comment in the public line in if your project uses Gatsby and not Next.js
93 | # https://nextjs.org/blog/next-9-1#public-directory-support
94 | # public
95 |
96 | # vuepress build output
97 | .vuepress/dist
98 |
99 | # Serverless directories
100 | .serverless/
101 |
102 | # FuseBox cache
103 | .fusebox/
104 |
105 | # DynamoDB Local files
106 | .dynamodb/
107 |
108 | # TernJS port file
109 | .tern-port
110 |
111 | # Stores VSCode versions used for testing VSCode extensions
112 | .vscode-test
113 |
114 | ### react ###
115 | .DS_*
116 | **/*.backup.*
117 | **/*.back.*
118 |
119 | node_modules
120 |
121 | *.sublime*
122 |
123 | psd
124 | thumb
125 | sketch
126 |
127 | # End of https://www.toptal.com/developers/gitignore/api/react,node
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "trailingComma": "es5",
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "tabWidth": 2,
7 | "useTabs": false
8 | }
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack: (config) => {
3 | config.module.rules.push({
4 | test: /\.svg$/,
5 | use: ['@svgr/webpack'],
6 | })
7 |
8 | return config
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chanho-boilerplate",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "next",
6 | "build": "npm run env -- next build && npm run env -- next export",
7 | "build2": "next build",
8 | "start": "next start",
9 | "type-check": "tsc"
10 | },
11 | "dependencies": {
12 | "@emotion/core": "^11.0.0",
13 | "@emotion/react": "^11.4.0",
14 | "@emotion/styled": "^11.3.0",
15 | "@material-ui/core": "^4.12.3",
16 | "@material-ui/icons": "^4.11.2",
17 | "@material-ui/lab": "^4.0.0-alpha.60",
18 | "@material-ui/styles": "^4.11.4",
19 | "@types/faker": "^5.5.8",
20 | "@types/react-cookies": "^0.1.0",
21 | "@types/react-icons": "^3.0.0",
22 | "axios": "^0.21.4",
23 | "emotion-reset": "^3.0.1",
24 | "faker": "^5.5.3",
25 | "immer": "^9.0.5",
26 | "next": "^11.0.1",
27 | "next-cookies": "^2.0.3",
28 | "next-redux-wrapper": "^7.0.2",
29 | "npm": "^8.1.3",
30 | "react": "^17.0.2",
31 | "react-cookies": "^0.1.1",
32 | "react-dom": "^17.0.2",
33 | "react-icons": "^4.2.0",
34 | "react-redux": "^7.2.4",
35 | "redux-saga": "^1.1.3"
36 | },
37 | "devDependencies": {
38 | "@emotion/babel-plugin": "^11.3.0",
39 | "@svgr/webpack": "^5.5.0",
40 | "@types/axios": "^0.14.0",
41 | "@types/node": "^16.4.4",
42 | "@types/react": "^17.0.15",
43 | "@types/react-dom": "^17.0.9",
44 | "@types/react-redux": "^7.1.18",
45 | "@typescript-eslint/eslint-plugin": "^4.28.5",
46 | "@typescript-eslint/parser": "^4.28.5",
47 | "eslint": "^7.31.0",
48 | "eslint-config-prettier": "^8.3.0",
49 | "eslint-plugin-jsx-a11y": "^6.4.1",
50 | "eslint-plugin-prettier": "^3.4.0",
51 | "eslint-plugin-react": "^7.24.0",
52 | "eslint-plugin-react-hooks": "^4.2.0",
53 | "prettier": "^2.3.2",
54 | "redux-devtools-extension": "^2.13.9",
55 | "typescript": "4.3"
56 | },
57 | "license": "MIT"
58 | }
59 |
--------------------------------------------------------------------------------
/public/images/alarmModal/already.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/alarmModal/checked.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/common/mic_team.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MIC-TEAM/wellseecoding-front/f810217b3dad26b64eb31a93d16308672abac11e/public/images/common/mic_team.png
--------------------------------------------------------------------------------
/public/images/common/update.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/footerMenu/menu01.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/footerMenu/menu02.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/public/images/footerMenu/menu03.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/header/FilledHeart.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/header/alarm.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/images/header/back.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/header/heart.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/header/nonAlarm.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/header/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/header/searchBack.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/header/setting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/login/character_color.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/images/login/email.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/images/login/naver.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/post/comment.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/public/images/post/comment_delet.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/post/recoment.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/public/images/post/upload.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/public/images/signup/check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/images/signup/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/images/signup/profile.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/images/splashScreen/logo01.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/images/splashScreen/logo2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/images/together/btn_delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/together/btn_write.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/apis/dummyData/index.tsx:
--------------------------------------------------------------------------------
1 | export const studyArr1 = [
2 | {
3 | id: 1,
4 | title: '[서울] 모각코 할 사람!',
5 | schedule: '올해 하반기까지 끝내는 것이 목표',
6 | qualification: '주 2회 / 온라인',
7 | summary: 'Swift UI가 아니라 UIkit 기반의 ...',
8 | peopleNum: '코로나 때문에 4명 전후로 모집할 예정입니다',
9 | hashtagArr: ['UI-KIT', 'IOS', '오프라인', '모여라'],
10 | },
11 | {
12 | id: 2,
13 | title: '[24/7] 모각코 하실분!',
14 | schedule: '올해 하반기까지 끝내는 것이 목표',
15 | qualification: '주 2회 / 온라인',
16 | summary: 'Swift UI가 아니라 UIkit 기반의 ...',
17 | peopleNum: '코로나 때문에 4명 전후로 모집할 예정입니다',
18 | hashtagArr: ['프론트엔드', '리액트', '온라인', '스터디'],
19 | },
20 | ]
21 |
22 | export const studyArr2 = [
23 | {
24 | id: 3,
25 | title: '[경기/안양] 스터디 구해요!',
26 | schedule: '올해 하반기까지 끝내는 것이 목표',
27 | qualification: '주 2회 / 온라인',
28 | summary: 'Swift UI가 아니라 UIkit 기반의 ...',
29 | peopleNum: '코로나 때문에 4명 전후로 모집할 예정입니다',
30 | hashtagArr: ['프론트엔드', '리액트', '온라인', '스터디'],
31 | },
32 | {
33 | id: 4,
34 | title: '[강남] 오프라인 토이프로젝트!',
35 | schedule: '올해 하반기까지 끝내는 것이 목표',
36 | qualification: '주 2회 / 오프라인',
37 | summary: 'Swift UI가 아니라 UIkit 기반의 ...',
38 | peopleNum: '코로나 때문에 4명 전후로 모집할 예정입니다',
39 | hashtagArr: ['백엔드', '노드', '익스프레스', 'MYSQL'],
40 | },
41 | ]
42 |
43 | export const studyArr3 = [
44 | {
45 | id: 5,
46 | title: '[강남] 오프라인 토이프로젝트!',
47 | schedule: '올해 하반기까지 끝내는 것이 목표',
48 | qualification: '주 2회 / 오프라인',
49 | summary: 'Swift UI가 아니라 UIkit 기반의 ...',
50 | peopleNum: '코로나 때문에 4명 전후로 모집할 예정입니다',
51 | hashtagArr: ['UIKIT', 'IOS', '오프라인', '스터디'],
52 | },
53 | {
54 | id: 6,
55 | title: '[역삼] 오프라인 토이프로젝트!',
56 | schedule: '올해 하반기까지 끝내는 것이 목표',
57 | qualification: '주 2회 / 오프라인',
58 | summary: 'Swift UI가 아니라 UIkit 기반의 ...',
59 | peopleNum: '코로나 때문에 4명 전후로 모집할 예정입니다',
60 | hashtagArr: ['토이프로젝트', '역삼역', '오프라인'],
61 | },
62 | {
63 | id: 7,
64 | title: '[서울] 모각코 할 사람!',
65 | schedule: '올해 하반기까지 끝내는 것이 목표',
66 | qualification: '주 2회 / 온라인',
67 | summary: 'Swift UI가 아니라 UIkit 기반의 ...',
68 | peopleNum: '코로나 때문에 4명 전후로 모집할 예정입니다',
69 | hashtagArr: ['UI-KIT', 'IOS', '오프라인', '모여라'],
70 | },
71 | ]
72 |
--------------------------------------------------------------------------------
/src/apis/index.tsx:
--------------------------------------------------------------------------------
1 | export const API_URL = `https://jsonplaceholder.typicode.com/todos`
2 |
3 | /* 후에 사가로 연결할 때는 필요 없을 것 같습니다*/
4 | export const GET_POSTS_URL = '/api/v1/posts'
5 | export const WRITE_POST_URL = '/api/v1/posts'
6 |
7 | // 이메일로 회원가입 한 것 로그인
8 | export const REGISTER_USERS_LOGIN = '/api/v1/users/token'
9 |
10 | // 회원가입 이름, 아이디, 비밀번호
11 | export const REGISTER_USERS_URL = '/api/v1/users'
12 |
13 | // 자기소개
14 | export const REGISTER_ABOUT_ME_URL = '/api/v1/users/profile/preface'
15 |
16 | // 학교정보
17 | export const REGISTER_EDUCATION_URL = '/api/v1/users/profile/education'
18 |
19 | // 경력정보
20 | export const REGISTER_WORK_URL = '/api/v1/users/profile/works'
21 |
22 | // 포트폴리오
23 | export const REGISTER_LINK_URL = '/api/v1/users/profile/links'
24 |
25 | // 현재 직업 (취준생, 학생, 직장인)
26 | export const REGISTER_STATUS_URL = '/api/v1/users/profile/status'
27 |
--------------------------------------------------------------------------------
/src/components/Alarm/List/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/click-events-have-key-events */
2 | /* eslint-disable jsx-a11y/no-static-element-interactions */
3 | import { css } from '@emotion/react'
4 | import { useRouter } from 'next/router'
5 | import { useCallback } from 'react'
6 | import { useDispatch } from 'react-redux'
7 | import { UPDATE_NOTI_REQUEST } from 'src/reducers/notifications'
8 | import { Common } from 'src/styles/common'
9 | import { notificationType } from 'src/types'
10 | // import Alarm from 'public/images/common/alarm.svg'
11 |
12 | type Props = {
13 | data: notificationType[]
14 | }
15 | const AlarmList = ({ data }: Props) => {
16 | const router = useRouter()
17 | const dispatch = useDispatch()
18 |
19 | const locationTo = useCallback(
20 | (id, query) => {
21 | router.push(`posts/${query}`).then(() => {
22 | dispatch({
23 | type: UPDATE_NOTI_REQUEST,
24 | data: Number(id),
25 | })
26 | })
27 | },
28 | [router, dispatch]
29 | )
30 |
31 | return (
32 |
33 | {data.map((v) => (
34 |
35 | {v.read ? (
36 |
locationTo(`${v.id}`, `${v.postId}`)}>
37 |
38 |
39 | {v.eventCategory === 'COMMENT_ADDED' && '댓글알림 '}
40 | {v.eventCategory === 'MEMBER_APPLIED' && '가입요청 '}
41 | {v.eventCategory === 'MEMBER_APPROVED' && '가입승인 '}
42 |
43 |
44 | {Math.floor((Date.now() / 1000 - v.timestamp) / 24 / 60 / 60) >= 1 ? (
45 |
{Math.floor((Date.now() / 1000 - v.timestamp) / 24 / 60 / 60)} 일전
46 | ) : (
47 |
오늘
48 | )}
49 |
50 |
51 |
52 | {v.eventCategory === 'COMMENT_ADDED' && `${v.senderUserName}님이 '${v.postTitle}' 글에 댓글을 달았어요`}
53 | {v.eventCategory === 'MEMBER_APPLIED' &&
54 | `${v.senderUserName}님이 ${v.receiverUserName}님의 '${v.postTitle}' 글에 가입신청했어요`}
55 | {v.eventCategory === 'MEMBER_APPROVED' &&
56 | `${v.receiverUserName}님이 요청하신 '${v.postTitle}' 글에 가입이 완료됐어요`}
57 |
58 |
59 | ) : (
60 |
locationTo(`${v.id}`, `${v.postId}`)}>
61 |
62 |
63 | {v.eventCategory === 'COMMENT_ADDED' && '댓글알림 '}
64 | {v.eventCategory === 'MEMBER_APPLIED' && '가입요청 '}
65 | {v.eventCategory === 'MEMBER_APPROVED' && '가입승인 '}
66 |
67 |
68 | {Math.floor((Date.now() / 1000 - v.timestamp) / 24 / 60 / 60) >= 1 ? (
69 |
{Math.floor((Date.now() / 1000 - v.timestamp) / 24 / 60 / 60)} 일전
70 | ) : (
71 |
오늘
72 | )}
73 |
74 |
75 |
76 | {v.eventCategory === 'COMMENT_ADDED' && `${v.senderUserName}님이 '${v.postTitle}' 글에 댓글을 달았어요`}
77 | {v.eventCategory === 'MEMBER_APPLIED' &&
78 | `${v.senderUserName}님이 ${v.receiverUserName}님의 '${v.postTitle}' 글에 가입신청했어요`}
79 | {v.eventCategory === 'MEMBER_APPROVED' &&
80 | `${v.receiverUserName}님이 요청하신 '${v.postTitle}' 글에 가입이 완료됐어요`}
81 |
82 |
83 | )}
84 |
85 | ))}
86 |
87 | )
88 | }
89 |
90 | export default AlarmList
91 |
92 | const alarmListBox = css`
93 | cursor: pointer;
94 | img {
95 | margin-right: 10px;
96 | }
97 | .on {
98 | background: #ffeee7;
99 | }
100 | & > div > div {
101 | &:nth-of-type(1) {
102 | border-top: 1px solid #efebe8;
103 | }
104 | padding: 20px;
105 | border-bottom: 1px solid #efebe8;
106 | }
107 | .header {
108 | font-size: ${Common.fontSize.fs14};
109 | letter-spacing: -0.4px;
110 | color: #8f8c8b;
111 | display: flex;
112 | justify-content: space-between;
113 | }
114 | p {
115 | font-size: ${Common.fontSize.fs16};
116 | line-height: 18px;
117 | letter-spacing: -0.8px;
118 | color: #262626;
119 | margin-top: 13px;
120 | margin-left: 22px;
121 | }
122 | `
123 |
--------------------------------------------------------------------------------
/src/components/Alarm/Title/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useEffect } from 'react'
2 | import { css } from '@emotion/react'
3 | import { Common } from 'src/styles/common'
4 | import { useDispatch } from 'react-redux'
5 | import { DELETE_ALL_NOTIS_REQUEST, READ_ALL_NOTIS_REQUEST } from 'src/reducers/notifications'
6 | import ConfirmModal from 'src/components/ConfirmModal'
7 |
8 | type Props = {
9 | num: number
10 | }
11 | const AlarmTitle = ({ num }: Props) => {
12 | /* 전체 삭제 모달 */
13 | const [deleteModalShowing, setDeleteModalShowing] = useState(false)
14 | /* 전체 삭제 모달 결과값 - delete modal confirm */
15 | const [dmConfirmResult, setDmConfirmResult] = useState(false)
16 | /* 전체 읽기 모달 */
17 | const [readModalShowing, setReadModalShowing] = useState(false)
18 | /* 전체 읽기 모달 결과값 = read modal confirm */
19 | const [rmConfirmResult, setRmConfirmResult] = useState(false)
20 |
21 | const dispatch = useDispatch()
22 |
23 | useEffect(() => {
24 | if (dmConfirmResult) {
25 | setDeleteModalShowing(false)
26 | setDmConfirmResult(false)
27 | deleteAll()
28 | }
29 | }, [dmConfirmResult])
30 |
31 | useEffect(() => {
32 | if (rmConfirmResult) {
33 | setReadModalShowing(false)
34 | setRmConfirmResult(false)
35 | readAll()
36 | }
37 | }, [rmConfirmResult])
38 |
39 | useEffect(() => {
40 | if (deleteModalShowing) document.body.style.overflow = 'hidden'
41 | else document.body.style.overflow = 'auto'
42 | }, [deleteModalShowing])
43 |
44 | const readAll = useCallback(() => {
45 | dispatch({
46 | type: READ_ALL_NOTIS_REQUEST,
47 | })
48 | }, [dispatch])
49 |
50 | const deleteAll = useCallback(() => {
51 | dispatch({
52 | type: DELETE_ALL_NOTIS_REQUEST,
53 | })
54 | }, [dispatch])
55 |
56 | const toggleReadModal = useCallback(() => {
57 | setReadModalShowing(true)
58 | }, [])
59 |
60 | const toggleDeleteModal = useCallback(() => {
61 | setDeleteModalShowing((prevState) => !prevState)
62 | document.body.style.overflow = 'hidden'
63 | }, [])
64 |
65 | return (
66 |
67 |
68 |
69 | {num !== 0 ? (
70 |
71 | {num} 개의 읽지 않은 알림이 있습니다.
72 |
73 | ) : (
74 | 읽지 않은 알림이 없습니다
75 | )}
76 |
77 |
78 |
79 |
80 | 전체 읽음
81 |
82 |
83 | 전체 삭제
84 |
85 |
86 |
87 |
88 | {readModalShowing && (
89 | setReadModalShowing(false)}
91 | confirmResult={() => setRmConfirmResult(true)}
92 | h3={'알림을 전체 읽음 표시 하시겠어요?'}
93 | p1={'내 서랍의 모든 알림이 읽음 표시로 처리됩니다'}
94 | p2={'읽음 표시한 알림은 이전 상태로 되돌릴 수 없습니다'}
95 | />
96 | )}
97 | {deleteModalShowing && (
98 | setDmConfirmResult(true)}
101 | h3={'알림을 모두 삭제 하시겠어요?'}
102 | p1={'내 서랍의 모든 알림이 삭제됩니다'}
103 | p2={'삭제된 알림은 다시 복구할 수 없습니다'}
104 | />
105 | )}
106 |
107 | )
108 | }
109 |
110 | export default AlarmTitle
111 |
112 | const alarmTitWrap = css`
113 | padding: 0 20px 22px;
114 | h1 {
115 | font-weight: 500;
116 | font-size: ${Common.fontSize.title};
117 | line-height: 36px;
118 | letter-spacing: -1px;
119 | color: #222222;
120 | }
121 | .allDelete {
122 | margin-left: 1.6em;
123 | }
124 | .desc {
125 | font-weight: 500;
126 | font-size: ${Common.fontSize.fs14};
127 | line-height: 17px;
128 | letter-spacing: -0.4px;
129 | display: flex;
130 | justify-content: space-between;
131 | margin-top: 0.4em;
132 | p {
133 | color: #262626;
134 | strong {
135 | color: #ff6e35;
136 | }
137 | }
138 | }
139 | `
140 |
--------------------------------------------------------------------------------
/src/components/AlarmModal/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | import { Modal } from './styles'
4 |
5 | interface onCloseProps {
6 | onClose: (event: React.MouseEvent) => void
7 | path: string
8 | text?: string
9 | textOpt?: string
10 | }
11 |
12 | const AlarmModal = ({ onClose, path, text, textOpt }: onCloseProps) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
{text}
19 |
{textOpt}
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default AlarmModal
27 |
--------------------------------------------------------------------------------
/src/components/AlarmModal/styles.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const Modal = css`
4 | width: 100%;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | background: rgba(0, 0, 0, 0.3);
13 | z-index: 9999;
14 | .modal {
15 | &__wrap {
16 | max-width: 600px;
17 | width: 100%;
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | position: relative;
22 | }
23 | &__box {
24 | padding: 30px;
25 | box-sizing: border;
26 | overflow: hidden;
27 | width: 77%;
28 | background: #ffffff;
29 | backdrop-filter: blur(12px);
30 | border-radius: 16px;
31 | text-align: center;
32 | h3 {
33 | font-weight: 700;
34 | font-size: 2.2rem;
35 | line-height: 26px;
36 | letter-spacing: -1px;
37 | color: #262626;
38 | padding-top: 33px;
39 |
40 | @media (max-width: 420px) {
41 | font-size: 1.7rem;
42 | }
43 | }
44 | p {
45 | font-size: 1.6rem;
46 | line-height: 22px;
47 | text-align: center;
48 | letter-spacing: -0.6px;
49 | color: #696766;
50 | padding: 10px 0 20px 0;
51 |
52 | @media (max-width: 420px) {
53 | font-size: 1.3rem;
54 | }
55 | }
56 | }
57 | }
58 | `
59 |
--------------------------------------------------------------------------------
/src/components/AuthLogin/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { css } from '@emotion/react'
3 | import { Common } from 'src/styles/common'
4 | // import KaKao from 'public/images/login/kakao.svg'
5 | // import Naver from 'public/images/login/naver.svg'
6 | // import Email from 'public/images/login/email.svg'
7 |
8 | export default function AuthLogin() {
9 | /* 배포용 */
10 | const kakaoLogin = () => (location.href = 'https://api.wellseecoding.com/oauth2/authorization/kakao')
11 | const naverLogin = () => (location.href = 'https://api.wellseecoding.com/oauth2/authorization/naver')
12 |
13 | /* 로컬용 */
14 | // const kakaoLogin = () => (location.href = 'http://localhost:8080/oauth2/authorization/kakao')
15 | // const naverLogin = () => (location.href = 'http://localhost:8080/oauth2/authorization/naver')
16 |
17 | return (
18 |
19 |
20 |
21 | 카카오톡으로 시작하기
22 |
23 |
24 |
25 | 네이버로 시작하기
26 |
27 |
(location.href = '/sign_in/email_start')}>
28 |
29 | 이메일로 시작하기
30 |
31 |
32 | 웰시가 처음이신가요?
33 |
34 | 회원가입
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | const authLoginButton = css`
42 | width: 100%;
43 | display: block;
44 | margin-top: 11em;
45 | padding: 0 20px;
46 | button {
47 | margin-top: 9px;
48 | width: 100%;
49 | border-radius: 16px;
50 | padding: 16px 0;
51 | font-weight: 500;
52 | font-size: ${Common.fontSize.fs18};
53 | position: relative;
54 | img,
55 | svg {
56 | position: absolute;
57 | left: 21px;
58 | top: 50%;
59 | transform: translateY(-50%);
60 | }
61 | }
62 | `
63 |
64 | const kakaoStyle = css`
65 | background: #fee500;
66 | color: #262626;
67 | `
68 |
69 | const naverStyle = css`
70 | color: #ffffff;
71 | background: #03c75a;
72 | `
73 |
74 | const email = css`
75 | color: #ffffff;
76 | background: #ff6e35;
77 | `
78 |
79 | const passwordFind = css`
80 | text-align: center;
81 | font-size: ${Common.fontSize.fs16};
82 | margin-top: 26px;
83 | color: #8f8c8b;
84 | a {
85 | margin-left: 4px;
86 | color: #ff6e35;
87 | font-weight: 500;
88 | text-decoration-line: underline;
89 | }
90 | `
91 |
--------------------------------------------------------------------------------
/src/components/Common/BigTitle/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { Common } from 'src/styles/common'
3 | // import Logo from 'public/images/login/character_color.svg'
4 |
5 | type Props = {
6 | title: string
7 | }
8 |
9 | export default function BigTitle({ title }: Props) {
10 | return (
11 | <>
12 |
13 | {title}
14 | >
15 | )
16 | }
17 |
18 | const bigTitleStyle = css`
19 | font-size: ${Common.fontSize.bigTitle};
20 | font-weight: 500;
21 | color: ${Common.colors.black};
22 | margin-top: 27px;
23 | `
24 |
--------------------------------------------------------------------------------
/src/components/Common/CommentModal/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/click-events-have-key-events */
2 | import React, { useCallback, useEffect } from 'react'
3 | import { css } from '@emotion/react'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import { CLOSE_EDITMODE, CLOSE_ISMODAL, OPEN_EDITMODE } from 'src/reducers/common'
6 | import { RootState } from 'src/reducers'
7 | import { DELETE_COMMENT_REQUEST } from 'src/reducers/comments'
8 | import { useRouter } from 'next/router'
9 | import usehandleOverFlow from 'src/hooks/useHandleOverflow'
10 |
11 | function CommentModal() {
12 | const dispatch = useDispatch()
13 | const router = useRouter()
14 |
15 | const { isModal } = useSelector((state: RootState) => state.common)
16 | const { deleteCommentSuccess } = useSelector((state: RootState) => state.comments)
17 |
18 | const { show } = usehandleOverFlow()
19 |
20 | useEffect(() => {
21 | if (deleteCommentSuccess) router.reload()
22 | }, [deleteCommentSuccess, router])
23 |
24 | const setModal = useCallback(
25 | (e) => {
26 | show()
27 | e.stopPropagation()
28 | dispatch({
29 | type: CLOSE_ISMODAL,
30 | })
31 | dispatch({
32 | type: CLOSE_EDITMODE,
33 | })
34 | },
35 | [dispatch, show]
36 | )
37 |
38 | const updatePost = useCallback(
39 | (e) => {
40 | show()
41 | e.stopPropagation()
42 | dispatch({
43 | type: CLOSE_ISMODAL,
44 | })
45 | dispatch({
46 | type: OPEN_EDITMODE,
47 | })
48 | },
49 | [dispatch, show]
50 | )
51 |
52 | const removePost = useCallback(
53 | (e, id) => {
54 | e.stopPropagation()
55 | dispatch({
56 | type: DELETE_COMMENT_REQUEST,
57 | data: {
58 | postId: Number(id),
59 | commentId: Number(isModal.uniqId),
60 | },
61 | })
62 | },
63 | [dispatch, isModal]
64 | )
65 |
66 | return (
67 | // eslint-disable-next-line jsx-a11y/no-static-element-interactions
68 |
69 |
70 |
71 |
72 | 수정
73 |
74 | removePost(e, isModal.uniqId)}>
75 | 삭제
76 |
77 |
78 |
79 |
80 | 취소
81 |
82 |
83 |
84 |
85 | )
86 | }
87 |
88 | export default CommentModal
89 |
90 | const modalWrap = css`
91 | background-color: rgba(196, 196, 196, 0.6);
92 | width: 100%;
93 | height: 100vh;
94 | z-index: 10500;
95 | position: absolute;
96 | top: 0;
97 | `
98 |
99 | const modalBtnWrap = css`
100 | padding: 20px 20px 28px 20px;
101 | display: block;
102 | position: absolute;
103 | bottom: 0px;
104 | width: 100%;
105 |
106 | button + button {
107 | margin-bottom: 10px;
108 | }
109 |
110 | div + div {
111 | background-color: #fff !important;
112 | }
113 | `
114 |
115 | const modalInner = css`
116 | display: block;
117 | background-color: #f1f1f1;
118 | border-radius: 14px;
119 |
120 | button + button {
121 | border-top: 1px solid rgba(196, 196, 196, 0.6);
122 | color: #fb4843 !important;
123 | }
124 |
125 | button {
126 | color: #1c81fa;
127 | padding: 18px;
128 | display: block;
129 | width: 100%;
130 | font-size: 18px;
131 | }
132 | `
133 |
--------------------------------------------------------------------------------
/src/components/Common/FlatBox/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { Common } from 'src/styles/common'
3 |
4 | type Props = {
5 | name: string
6 | contents: string
7 | }
8 |
9 | function FlatBox({ name, contents }: Props) {
10 | return (
11 |
12 |
{name}
13 |
{contents}
14 |
15 | )
16 | }
17 |
18 | export default FlatBox
19 |
20 | const FlatWrap = css`
21 | background-color: #fff;
22 | padding: 21px;
23 | margin-bottom: 9px;
24 |
25 | h3 {
26 | font-size: ${Common.fontSize.fs18};
27 | font-weight: 500;
28 | margin-bottom: 8px;
29 | }
30 |
31 | p {
32 | font-size: ${Common.fontSize.fs16};
33 | font-weight: 500;
34 | line-height: 22px;
35 | letter-spacing: -0.6px;
36 | }
37 | `
38 |
--------------------------------------------------------------------------------
/src/components/Common/FootButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes } from 'react'
2 | import { css } from '@emotion/react'
3 | import { Common } from 'src/styles/common'
4 |
5 | export enum FootButtonType {
6 | DISABLE = 'disable',
7 | ACTIVATION = 'activation',
8 | SKIP = 'skip',
9 | }
10 |
11 | interface IProps extends ButtonHTMLAttributes {
12 | footButtonType: FootButtonType
13 | }
14 |
15 | function FootButton({ children, type, footButtonType, ...props }: IProps) {
16 | return (
17 |
24 | {children}
25 |
26 | )
27 | }
28 |
29 | export default FootButton
30 |
31 | const container = css`
32 | width: 100%;
33 | height: 52px;
34 | display: block;
35 | font-size: ${Common.fontSize.fs18};
36 | border-radius: 16px;
37 | padding: 16px 0;
38 | font-style: normal;
39 | font-weight: 500;
40 | line-height: 20px;
41 | color: #ffffff;
42 | background: ${Common.colors.gray04};
43 |
44 | &:disabled {
45 | background: ${Common.colors.gray04};
46 | cursor: default;
47 | }
48 |
49 | &.activation {
50 | background: #ff6e35;
51 | }
52 |
53 | &.skip {
54 | background: #ffffff;
55 | border: 1px solid #ff6e35;
56 | color: ${Common.colors.black};
57 | }
58 | `
59 |
--------------------------------------------------------------------------------
/src/components/Common/HashWrap/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { Common } from 'src/styles/common'
3 |
4 | type Props = {
5 | content: string
6 | }
7 |
8 | function HashWrap({ content }: Props) {
9 | return #{content}
10 | }
11 |
12 | export default HashWrap
13 |
14 | const hashWrap = css`
15 | background: #ffeee7;
16 | border-radius: 56px;
17 | padding: 8px 12px;
18 | color: #ff6e35;
19 | display: inline-block;
20 | font-weight: bold;
21 | font-size: ${Common.fontSize.fs14};
22 | line-height: 20px;
23 | margin-right: 4px;
24 | margin-top: 4px;
25 | `
26 |
--------------------------------------------------------------------------------
/src/components/Common/Header/Back/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { useRouter } from 'next/router'
3 | import { Common } from 'src/styles/common'
4 | /* 웹팩 설정을 통해 pre-render 담당 */
5 | // import BackSvg from '/public/images/header/back.svg'
6 |
7 | type Props = {
8 | text: string
9 | }
10 |
11 | function Back({ text }: Props) {
12 | const router = useRouter()
13 |
14 | return (
15 |
16 | router.back()}>
17 |
18 |
19 | {text}
20 |
21 | )
22 | }
23 |
24 | export default Back
25 |
26 | Back.defaultProps = {
27 | text: '',
28 | }
29 |
30 | const backHeader = css`
31 | width: 100%;
32 | text-align: left;
33 | background: #fff;
34 | top: 0;
35 | padding: 0 20px;
36 | display: flex;
37 | align-items: center;
38 | button {
39 | display: flex;
40 | justify-content: center;
41 | align-items: center;
42 | }
43 | h1 {
44 | font-weight: 500;
45 | font-size: ${Common.fontSize.fs20};
46 | line-height: 28px;
47 | letter-spacing: -0.4px;
48 | color: #262626;
49 | margin-left: -30px;
50 | }
51 | img {
52 | width: 100%;
53 | height: 100%;
54 | }
55 | `
56 |
--------------------------------------------------------------------------------
/src/components/Common/Header/EditBackOptional/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import usehandleOverFlow from 'src/hooks/useHandleOverflow'
3 | import { useCallback } from 'react'
4 | import { useDispatch } from 'react-redux'
5 | import { CLOSE_EDITMODE } from 'src/reducers/common'
6 | import { Common } from 'src/styles/common'
7 |
8 | type Props = {
9 | text: string
10 | }
11 |
12 | function EditBack({ text }: Props) {
13 | const dispatch = useDispatch()
14 | const { show } = usehandleOverFlow()
15 |
16 | const closeModal = useCallback(() => {
17 | show()
18 | dispatch({
19 | type: CLOSE_EDITMODE,
20 | })
21 | }, [show, dispatch])
22 |
23 | return (
24 |
25 |
26 |
27 |
28 | {text}
29 |
30 | )
31 | }
32 |
33 | export default EditBack
34 |
35 | EditBack.defaultProps = {
36 | text: '',
37 | }
38 |
39 | const backHeader = css`
40 | width: 100%;
41 | text-align: left;
42 | position: sticky;
43 | background: #fff;
44 | /* top: 0; */
45 | padding: 0 20px;
46 | display: flex;
47 | align-items: center;
48 | button {
49 | display: flex;
50 | justify-content: center;
51 | align-items: center;
52 | }
53 | h1 {
54 | font-weight: 500;
55 | font-size: ${Common.fontSize.fs20};
56 | line-height: 28px;
57 | letter-spacing: -0.4px;
58 | color: #262626;
59 | margin-left: -30px;
60 | }
61 |
62 | img {
63 | width: 100%;
64 | height: 100%;
65 | }
66 | `
67 |
--------------------------------------------------------------------------------
/src/components/Common/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import useHeader from 'src/hooks/useHeader'
2 | import { css } from '@emotion/react'
3 |
4 | function Header() {
5 | const { teamName } = useHeader()
6 |
7 | return {teamName}
8 | }
9 |
10 | export default Header
11 |
12 | const container = css`
13 | margin: 15px 0;
14 | text-align: center;
15 | font-size: 20px;
16 | `
17 |
--------------------------------------------------------------------------------
/src/components/Common/IsModal/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | import React, { useCallback, useEffect } from 'react'
4 | import { css } from '@emotion/react'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import { CLOSE_ISMODAL, OPEN_EDITMODE } from 'src/reducers/common'
7 | import { RootState } from 'src/reducers'
8 | import { DELETE_POST_REQUEST } from 'src/reducers/posts'
9 | import usehandleOverFlow from 'src/hooks/useHandleOverflow'
10 |
11 | const IsModal = () => {
12 | const { hidden, show } = usehandleOverFlow()
13 | const dispatch = useDispatch()
14 |
15 | const { isModal } = useSelector((state: RootState) => state.common)
16 | const { deletePostSuccess } = useSelector((state: RootState) => state.posts)
17 |
18 | // 내 게시글 삭제시 오류 때문에 일단 멈춰둠
19 |
20 | useEffect(() => {
21 | if (deletePostSuccess) {
22 | location.replace('/home')
23 | }
24 | }, [deletePostSuccess])
25 |
26 | useEffect(() => {
27 | isModal && hidden()
28 | }, [isModal, hidden])
29 |
30 | const setModal = useCallback(
31 | (e) => {
32 | e.stopPropagation()
33 | dispatch({
34 | type: CLOSE_ISMODAL,
35 | })
36 | show()
37 | },
38 | [dispatch, show]
39 | )
40 |
41 | const updatePost = useCallback(
42 | (e) => {
43 | e.stopPropagation()
44 | dispatch({
45 | type: CLOSE_ISMODAL,
46 | })
47 | dispatch({
48 | type: OPEN_EDITMODE,
49 | })
50 | },
51 | [dispatch]
52 | )
53 |
54 | const removePost = useCallback(
55 | (e, id) => {
56 | e.stopPropagation()
57 | dispatch({
58 | type: DELETE_POST_REQUEST,
59 | data: id,
60 | })
61 | },
62 | [dispatch]
63 | )
64 |
65 | return (
66 |
67 |
68 |
69 |
70 | 수정
71 |
72 | removePost(e, Number(isModal.uniqId))}>
73 | 삭제
74 |
75 |
76 |
77 |
78 | 취소
79 |
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | export default IsModal
87 |
88 | const modalWrap = css`
89 | background-color: rgba(196, 196, 196, 0.6);
90 | width: 100%;
91 | height: 100vh;
92 | z-index: 10500;
93 | position: absolute;
94 | top: 0;
95 | `
96 |
97 | const modalBtnWrap = css`
98 | padding: 20px 20px 28px 20px;
99 | display: block;
100 | position: absolute;
101 | bottom: 0px;
102 | width: 100%;
103 |
104 | button + button {
105 | margin-bottom: 10px;
106 | }
107 |
108 | div + div {
109 | background-color: #fff !important;
110 | }
111 | `
112 |
113 | const modalInner = css`
114 | display: block;
115 | background-color: #f1f1f1;
116 | border-radius: 14px;
117 |
118 | button + button {
119 | border-top: 1px solid rgba(196, 196, 196, 0.6);
120 | color: #fb4843 !important;
121 | }
122 |
123 | button {
124 | color: #1c81fa;
125 | padding: 18px;
126 | display: block;
127 | width: 100%;
128 | font-size: 18px;
129 | }
130 | `
131 |
--------------------------------------------------------------------------------
/src/components/Common/JobButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | type Props = {
4 | job_text: string
5 | onClick: (e: React.MouseEvent) => void
6 | className: string
7 | }
8 |
9 | function JobButton({ job_text, onClick, className }: Props) {
10 | return (
11 |
12 | #{job_text}
13 |
14 | )
15 | }
16 |
17 | export default JobButton
18 |
19 | const hashTag = css`
20 | background: #ffffff;
21 | border: 1px solid #d3cfcc;
22 | color: #d3cfcc;
23 | border-radius: 60px;
24 | display: inline-block;
25 | padding: 10px 14px;
26 | margin-top: 14px;
27 | margin-right: 8px;
28 | &:focus {
29 | border: 1px solid #ff6e35;
30 | color: #ff6e35;
31 | }
32 | p {
33 | font-size: 1.6rem;
34 | }
35 | `
36 |
--------------------------------------------------------------------------------
/src/components/Common/PasswordField/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Input from '@material-ui/core/Input'
3 | import InputLabel from '@material-ui/core/InputLabel'
4 | import FormControl from '@material-ui/core/FormControl'
5 |
6 | interface State {
7 | amount: string
8 | password: string
9 | weight: string
10 | weightRange: string
11 | showPassword: boolean
12 | }
13 |
14 | type Props = {
15 | title: string
16 | passwordText: string
17 | typeTitle: string
18 | onChange: (e: React.ChangeEvent) => void
19 | }
20 |
21 | export default function PasswordField({ title, passwordText, typeTitle, onChange }: Props) {
22 | const [values, setValues] = useState({
23 | amount: '',
24 | password: '',
25 | weight: '',
26 | weightRange: '',
27 | showPassword: false,
28 | })
29 |
30 | const handleChange = (key: keyof State) => (event: React.ChangeEvent) => {
31 | setValues({ ...values, [key]: event.target.value })
32 | onChange(event)
33 | }
34 |
35 | return (
36 |
37 | {title}
38 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Common/TextField/index.tsx:
--------------------------------------------------------------------------------
1 | import TextField from '@material-ui/core/TextField'
2 |
3 | type Props = {
4 | text: string
5 | type: string
6 | typeName: string
7 | onChange: (e: React.ChangeEvent) => void
8 | }
9 |
10 | export default function TextFields({ text, typeName, type, onChange }: Props) {
11 | return
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Common/TextFieldProfile/index.tsx:
--------------------------------------------------------------------------------
1 | import TextField from '@material-ui/core/TextField'
2 |
3 | type Props = {
4 | text: string
5 | type: string
6 | onChange: (e: React.ChangeEvent) => void
7 | onKeyUp?: (e: React.ChangeEvent) => void
8 | placeholder?: string
9 | value?: string | number
10 | name?: string
11 | }
12 |
13 | export default function TextFields({ text, type, onChange, placeholder, value }: Props) {
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Common/Title/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { Common } from 'src/styles/common'
3 |
4 | type Props = {
5 | title: string
6 | className?: string
7 | }
8 |
9 | export default function Title({ title, className }: Props) {
10 | return (
11 |
12 | {title}
13 |
14 | )
15 | }
16 |
17 | const titleStyle = css`
18 | font-size: ${Common.fontSize.title};
19 | font-weight: 500;
20 | color: ${Common.colors.black};
21 | padding-left: 20px;
22 | padding-top: 3.7em;
23 | `
24 |
--------------------------------------------------------------------------------
/src/components/Common/wellseeError/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import React from 'react'
3 | import { Common } from 'src/styles/common'
4 |
5 | export type Props = {
6 | text: string
7 | textOpt?: string
8 | buttonOpt?: string
9 | }
10 |
11 | const WellseeError = ({ text, textOpt, buttonOpt }: Props) => {
12 | return (
13 |
14 |
15 |
16 |
17 | {text}
18 |
19 |
20 | {textOpt && (
21 |
22 | {textOpt}
23 |
24 | )}
25 |
26 | {buttonOpt && (
27 |
28 | (location.href = '/together')}>
29 | {buttonOpt}
30 |
31 |
32 | )}
33 |
34 |
35 | )
36 | }
37 |
38 | export default WellseeError
39 |
40 | const errorWrap = css`
41 | display: flex;
42 | width: 100%;
43 | justify-content: center;
44 | height: 95vh;
45 | align-items: center;
46 | flex-direction: column;
47 | background: #ffeee7;
48 | `
49 |
50 | const footButtonWrapper = css`
51 | font-size: ${Common.fontSize.fs16};
52 | margin-top: 30px;
53 | padding: 16px;
54 | border-radius: 16px;
55 | background-color: #ff6e35;
56 | color: #ffffff;
57 | `
58 |
--------------------------------------------------------------------------------
/src/components/Common/wellseeErrorHome/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import React from 'react'
3 | import { Common } from 'src/styles/common'
4 |
5 | export type Props = {
6 | text: string
7 | textOpt?: string
8 | buttonOpt?: string
9 | }
10 |
11 | const WellseeErrorHome = ({ text, textOpt, buttonOpt }: Props) => {
12 | return (
13 |
14 |
15 |
16 |
17 | {text}
18 |
19 |
20 | {textOpt && (
21 |
22 | {textOpt}
23 |
24 | )}
25 |
26 | {buttonOpt && (
27 |
28 | (location.href = '/together')}>
29 | {buttonOpt}
30 |
31 |
32 | )}
33 |
34 |
35 | )
36 | }
37 |
38 | export default WellseeErrorHome
39 |
40 | const ErrorOpt = css`
41 | background: #ffeee7;
42 | margin-top: 3em;
43 | padding-top: 10em;
44 | display: flex;
45 | width: 100%;
46 | justify-content: center;
47 | height: 100%;
48 | align-items: center;
49 | flex-direction: column;
50 | background: #ffeee7;
51 |
52 | @media (max-width: 420px) {
53 | padding-top: 3em;
54 | }
55 | `
56 |
57 | const footButtonWrapper = css`
58 | font-size: ${Common.fontSize.fs16};
59 | margin-top: 30px;
60 | padding: 16px;
61 | border-radius: 16px;
62 | background-color: #ff6e35;
63 | color: #ffffff;
64 | `
65 |
--------------------------------------------------------------------------------
/src/components/ConfirmModal/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/click-events-have-key-events */
2 | /* eslint-disable jsx-a11y/no-static-element-interactions */
3 | import { Modal } from './styles'
4 |
5 | interface Props {
6 | onClose: (event: React.MouseEvent) => void
7 | confirmResult: () => void
8 | h3: string
9 | p1: string
10 | p2?: string
11 | }
12 |
13 | const ConfirmModal = ({ onClose, confirmResult, h3, p1, p2 }: Props) => {
14 | return (
15 |
16 |
17 |
18 |
19 |
{h3}
20 |
21 | {p1}
22 |
23 | {p2}
24 |
25 |
26 |
27 | 취소
28 |
29 |
30 | 확인
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | export default ConfirmModal
40 |
--------------------------------------------------------------------------------
/src/components/ConfirmModal/styles.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const Modal = css`
4 | width: 100%;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | background: rgba(0, 0, 0, 0.3);
13 | z-index: 9999;
14 | .modal {
15 | &__wrap {
16 | max-width: 600px;
17 | width: 100%;
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | position: relative;
22 | img {
23 | position: absolute;
24 | top: -78px;
25 | z-index: 100;
26 | }
27 | }
28 | &__box {
29 | box-sizing: border;
30 | overflow: hidden;
31 | width: 77%;
32 | background: #ffffff;
33 | backdrop-filter: blur(12px);
34 | border-radius: 16px;
35 | text-align: center;
36 | h3 {
37 | font-weight: 700;
38 | font-size: 2.2rem;
39 | line-height: 26px;
40 | letter-spacing: -1px;
41 | color: #262626;
42 | padding-top: 33px;
43 |
44 | @media (max-width: 420px) {
45 | font-size: 1.6rem;
46 | }
47 | }
48 | p {
49 | font-size: 1.6rem;
50 | line-height: 22px;
51 | text-align: center;
52 | letter-spacing: -0.6px;
53 | color: #696766;
54 | padding: 10px 0 20px 0;
55 |
56 | @media (max-width: 420px) {
57 | font-size: 1.2rem;
58 | }
59 | }
60 | }
61 | &__btn {
62 | display: flex;
63 | justify-content: center;
64 | align-content: center;
65 | width: 100%;
66 | border-top: 1px solid #efebe8;
67 | button {
68 | padding: 18px 0;
69 | font-size: 2rem;
70 | width: 50%;
71 | box-sizing: border-box;
72 |
73 | @media (max-width: 420px) {
74 | font-size: 1.5rem;
75 | }
76 | &.delete {
77 | color: #ff6e35;
78 | border-left: 1px solid #efebe8;
79 | }
80 | }
81 | }
82 | }
83 | `
84 |
--------------------------------------------------------------------------------
/src/components/DataForm/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TodoType } from 'src/types'
3 | import { MainContent } from './style'
4 |
5 | type Props = {
6 | datas: TodoType[] | undefined
7 | }
8 |
9 | function DataForm(props: Props) {
10 | const { datas } = props
11 |
12 | return (
13 |
14 | {datas &&
15 | datas.map((data) => (
16 |
17 | {data.completed === false ? non-completed
: completed
}
18 | userId: {data.userId}
19 | title: {data.title}
20 |
21 | ))}
22 |
23 | )
24 | }
25 |
26 | export default DataForm
27 |
--------------------------------------------------------------------------------
/src/components/DataForm/style.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled'
2 |
3 | interface StyledDivProps {
4 | complete: boolean
5 | }
6 |
7 | export const MainContent = styled.div`
8 | border: 1px solid black;
9 | margin: 20px 0;
10 | padding: 20px;
11 |
12 | background-color: ${(props) => props.complete === false && 'salmon'};
13 |
14 | & p {
15 | font-weight: bolder;
16 | }
17 | `
18 |
--------------------------------------------------------------------------------
/src/components/Home/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { css } from '@emotion/react'
3 |
4 | type Props = {
5 | notis: boolean
6 | }
7 |
8 | function HomeHeader({ notis }: Props) {
9 | return (
10 |
23 | )
24 | }
25 |
26 | export default HomeHeader
27 |
28 | export const HomeHeaderWrap = css`
29 | display: flex;
30 | justify-content: flex-end;
31 | padding: 0 20px;
32 | `
33 |
--------------------------------------------------------------------------------
/src/components/Home/Main/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { Common } from 'src/styles/common'
3 |
4 | interface Props {
5 | user: string | null
6 | num?: number
7 | }
8 | function HomeMain({ user, num }: Props) {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | 안녕하세요 {user}님 👋
16 |
17 |
18 | {num ? (
19 |
20 | 가입하신 스터디는
21 | 총 {num}개에요~
22 |
23 | ) : (
24 |
25 | 아직 가입하신
26 | 스터디가 없어요.. 🥲
27 |
28 | )}
29 |
30 |
31 |
32 | )
33 | }
34 |
35 | export default HomeMain
36 |
37 | export const homeMainWrap = css`
38 | position: relative;
39 | background-color: white;
40 | display: flex;
41 | justify-content: center;
42 |
43 | img {
44 | z-index: 10;
45 | position: absolute;
46 | left: 5%;
47 | clip: rect(0px, 220px, 100px, 0px);
48 | }
49 | div {
50 | margin-top: 1em;
51 | margin-left: 1em;
52 | @media (max-width: 420px) {
53 | margin-left: 12em;
54 | }
55 | h1 {
56 | font-weight: 500;
57 | font-size: ${Common.fontSize.fs18};
58 | line-height: 26px;
59 | color: #262626;
60 | letter-spacing: -0.6px;
61 | @media (max-width: 420px) {
62 | font-size: ${Common.fontSize.fs16};
63 | }
64 | }
65 | p {
66 | font-weight: 500;
67 | font-size: ${Common.fontSize.title};
68 | line-height: 32px;
69 | letter-spacing: -1px;
70 | color: #262626;
71 | @media (max-width: 420px) {
72 | font-size: ${Common.fontSize.fs16};
73 | }
74 | }
75 | }
76 | `
77 |
--------------------------------------------------------------------------------
/src/components/Home/StudySection/index.tsx:
--------------------------------------------------------------------------------
1 | import StudySlider from 'src/components/Together/StudySlider'
2 | import { css } from '@emotion/react'
3 | import { PostType } from 'src/types'
4 |
5 | // data는 dataProps { } 객체 형식으로 이루어진 배열이다
6 |
7 | type Props = {
8 | title: string
9 | data: PostType[]
10 | }
11 |
12 | function StudyTitle({ title, data }: Props) {
13 | return (
14 |
15 |
16 | {title}
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default StudyTitle
25 |
26 | const titleStyle = css`
27 | font-size: 2.2rem;
28 | color: #262626;
29 | font-weight: 500;
30 | margin-top: 22px;
31 | margin-left: 20px;
32 | margin-bottom: 16px;
33 | `
34 |
--------------------------------------------------------------------------------
/src/components/LikePost/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | interface Props {
3 | title: string
4 | name: string
5 | date: number
6 | }
7 |
8 | function LikePostList({ title, name, date }: Props) {
9 | return (
10 |
11 | {title}
12 |
13 |
{name} | {date}
14 |
15 |
16 | )
17 | }
18 |
19 | export default LikePostList
20 |
21 | const list = css`
22 | border-bottom: 1px solid #d3cfcc;
23 | padding: 20px 0;
24 | h3 {
25 | font-size: 1.6rem;
26 | line-height: 22px;
27 | letter-spacing: -0.6px;
28 | color: #262626;
29 | }
30 | div {
31 | margin-top: 4px;
32 | font-size: 1.6rem;
33 | line-height: 22px;
34 | letter-spacing: -0.6px;
35 | color: rgba(131, 131, 131, 0.87);
36 | display: flex;
37 | }
38 | h5 {
39 | margin-right: 0.5rem;
40 | }
41 | span {
42 | margin-left: 0.5rem;
43 | }
44 | `
45 |
--------------------------------------------------------------------------------
/src/components/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import React from 'react'
3 | import { Common } from 'src/styles/common'
4 |
5 | const Loading = () => {
6 | return (
7 | <>
8 | loading...
9 | >
10 | )
11 | }
12 |
13 | const loading = css`
14 | font-size: ${Common.fontSize.title};
15 | width: 100%;
16 | height: 100vh;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | `
21 |
22 | export default Loading
23 |
--------------------------------------------------------------------------------
/src/components/LogOutModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { Modal } from './styles'
3 |
4 | interface onCloseProps {
5 | onClose: (event: React.MouseEvent) => void
6 | }
7 |
8 | const MoreModal = (props: onCloseProps) => {
9 | const logOut = useCallback(() => {
10 | if (typeof window !== 'undefined') {
11 | localStorage.clear()
12 | eraseCookie('access_token')
13 | props.onClose
14 | location.replace('/sign_in/auth_start')
15 | }
16 | }, [])
17 |
18 | function eraseCookie(name: string) {
19 | document.cookie = name + '=; Max-Age=0'
20 | }
21 | return (
22 |
23 |
24 |
25 |
26 |
로그아웃
27 |
28 | 아직 다양한 모임들이 참여를 기다리고 있어요!
29 |
30 | 그래도 로그아웃 하시겠어요?
31 |
32 |
33 |
34 | 취소
35 |
36 |
37 | 로그아웃
38 |
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default MoreModal
47 |
--------------------------------------------------------------------------------
/src/components/LogOutModal/styles.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const Modal = css`
4 | width: 100%;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | background: rgba(0, 0, 0, 0.3);
13 | z-index: 100;
14 | .modal {
15 | &__wrap {
16 | max-width: 600px;
17 | width: 100%;
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | position: relative;
22 | img {
23 | position: absolute;
24 | top: -78px;
25 | z-index: 100;
26 | }
27 | }
28 | &__box {
29 | box-sizing: border;
30 | overflow: hidden;
31 | width: 77%;
32 | background: #ffffff;
33 | backdrop-filter: blur(12px);
34 | border-radius: 16px;
35 | text-align: center;
36 | h3 {
37 | font-weight: 700;
38 | font-size: 2.2rem;
39 | line-height: 26px;
40 | letter-spacing: -1px;
41 | color: #262626;
42 | padding-top: 33px;
43 | }
44 | p {
45 | font-size: 1.6rem;
46 | line-height: 22px;
47 | text-align: center;
48 | letter-spacing: -0.6px;
49 | color: #696766;
50 | padding: 10px 0 20px 0;
51 | }
52 | }
53 | &__btn {
54 | display: flex;
55 | justify-content: center;
56 | align-content: center;
57 | width: 100%;
58 | border-top: 1px solid #efebe8;
59 | button {
60 | padding: 18px 0;
61 | font-size: 2rem;
62 | width: 50%;
63 | box-sizing: border-box;
64 | &.delete {
65 | color: #ff6e35;
66 | border-left: 1px solid #efebe8;
67 | }
68 | }
69 | }
70 | }
71 | `
72 |
--------------------------------------------------------------------------------
/src/components/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | import { Modal } from './styles'
2 |
3 | interface onCloseProps {
4 | onClose: (event: React.MouseEvent) => void
5 | }
6 |
7 | const MoreModal = (props: onCloseProps) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
알림을 모두 삭제 하시겠어요?
14 |
15 | 내 서랍의 모든 알림이 삭제됩니다.
16 | 삭제된 알림은 다시 복구할 수 없습니다.
17 |
18 |
19 |
20 | 취소
21 |
22 | 삭제
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default MoreModal
31 |
--------------------------------------------------------------------------------
/src/components/Modal/styles.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const Modal = css`
4 | width: 100%;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | background: rgba(0, 0, 0, 0.3);
13 | z-index: 100;
14 | .modal {
15 | &__wrap {
16 | max-width: 600px;
17 | width: 100%;
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | position: relative;
22 | img {
23 | position: absolute;
24 | top: -78px;
25 | z-index: 100;
26 | }
27 | }
28 | &__box {
29 | box-sizing: border;
30 | overflow: hidden;
31 | width: 77%;
32 | background: #ffffff;
33 | backdrop-filter: blur(12px);
34 | border-radius: 16px;
35 | text-align: center;
36 | h3 {
37 | font-weight: 700;
38 | font-size: 2.2rem;
39 | line-height: 26px;
40 | letter-spacing: -1px;
41 | color: #262626;
42 | padding-top: 33px;
43 | }
44 | p {
45 | font-size: 1.6rem;
46 | line-height: 22px;
47 | text-align: center;
48 | letter-spacing: -0.6px;
49 | color: #696766;
50 | padding: 10px 0 20px 0;
51 | }
52 | }
53 | &__btn {
54 | display: flex;
55 | justify-content: center;
56 | align-content: center;
57 | width: 100%;
58 | border-top: 1px solid #efebe8;
59 | button {
60 | padding: 18px 0;
61 | font-size: 2rem;
62 | width: 50%;
63 | box-sizing: border-box;
64 | &.delete {
65 | color: #ff6e35;
66 | border-left: 1px solid #efebe8;
67 | }
68 | }
69 | }
70 | }
71 | `
72 |
--------------------------------------------------------------------------------
/src/components/MyPage/Career/index.tsx:
--------------------------------------------------------------------------------
1 | import { box } from './style'
2 | import React, { useCallback, useState } from 'react'
3 | import AlarmModal from 'src/components/AlarmModal'
4 |
5 | interface CareerProps {
6 | company: string
7 | job: string
8 | year: number
9 | }
10 |
11 | const Career = (props: CareerProps) => {
12 | const myInfo = JSON.stringify(localStorage.getItem('access_token'))
13 | const [confirmModal, setConfirmModal] = useState(false)
14 |
15 | // 이미 가입된 알림 모달 끄기
16 | const closeModal = useCallback(() => {
17 | setConfirmModal(false)
18 | }, [])
19 | return (
20 |
21 |
22 | 경력 총 {props.year}년차
23 |
24 |
25 | {/* 회사이름 */}
26 | {props.company}
27 | {/* 직업군 | 기술스택 년도 */}
28 |
29 | 기술스택 | {props.job} | 경력 | {props.year}년차
30 |
31 |
32 | {myInfo ? (
33 | setConfirmModal(true)}>
34 |
35 |
36 | ) : (
37 |
38 | )}
39 | {confirmModal && (
40 |
45 | )}
46 |
47 | )
48 | }
49 |
50 | export default Career
51 |
--------------------------------------------------------------------------------
/src/components/MyPage/Career/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const box = css`
4 | background: #ffffff;
5 | border: 1px solid #ffeee7;
6 | box-sizing: border-box;
7 | box-shadow: 0px 7px 24px rgb(0 0 0 / 10%);
8 | border-radius: 10px;
9 | margin-bottom: 18px;
10 | padding: 26px;
11 | position: relative;
12 | button {
13 | position: absolute;
14 | right: 26px;
15 | top: 26px;
16 | }
17 | h2 {
18 | font-weight: bold;
19 | font-size: 2rem;
20 | line-height: 22px;
21 | letter-spacing: 0.15px;
22 | color: rgba(0, 0, 0, 0.87);
23 | margin-bottom: 17px;
24 | strong {
25 | color: #ff6e35;
26 | margin-left: 4px;
27 | font-weight: 500;
28 | }
29 | }
30 | p {
31 | font-weight: 500;
32 | font-size: 1.8rem;
33 | line-height: 26px;
34 | display: flex;
35 | letter-spacing: -0.6px;
36 | color: #444241;
37 | }
38 | .company {
39 | font-weight: bold;
40 | font-size: 1.8rem;
41 | line-height: 26px;
42 | letter-spacing: -0.6px;
43 | color: #444241;
44 | }
45 | .desc {
46 | font-size: 1.6rem;
47 | line-height: 22px;
48 | letter-spacing: -0.6px;
49 | color: rgba(131, 131, 131, 0.87);
50 | margin-top: 3px;
51 | }
52 | `
53 |
--------------------------------------------------------------------------------
/src/components/MyPage/Portfolio/index.tsx:
--------------------------------------------------------------------------------
1 | import { box } from './style'
2 | import React, { useCallback, useState } from 'react'
3 | import AlarmModal from 'src/components/AlarmModal'
4 |
5 | interface PortfolioProps {
6 | link: string
7 | name: string
8 | description: string
9 | }
10 |
11 | const Portfolio = (props: PortfolioProps) => {
12 | const myInfo = JSON.stringify(localStorage.getItem('access_token'))
13 | const [confirmModal, setConfirmModal] = useState(false)
14 |
15 | // 이미 가입된 알림 모달 끄기
16 | const closeModal = useCallback(() => {
17 | setConfirmModal(false)
18 | }, [])
19 |
20 | return (
21 |
22 | 포트폴리오
23 |
24 |
25 | {props.name}
26 |
27 |
28 |
29 | {props.link}
30 |
31 |
32 | {props.description}
33 |
34 | {myInfo ? (
35 | setConfirmModal(true)}>
36 |
37 |
38 | ) : (
39 |
40 | )}
41 |
42 | {confirmModal && (
43 |
48 | )}
49 |
50 | )
51 | }
52 |
53 | export default Portfolio
54 |
--------------------------------------------------------------------------------
/src/components/MyPage/Portfolio/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const box = css`
4 | background: #ffffff;
5 | border: 1px solid #ffeee7;
6 | box-sizing: border-box;
7 | box-shadow: 0px 7px 24px rgb(0 0 0 / 10%);
8 | border-radius: 10px;
9 | margin-bottom: 18px;
10 | padding: 26px;
11 | position: relative;
12 | button {
13 | position: absolute;
14 | right: 26px;
15 | top: 26px;
16 | }
17 | h2 {
18 | font-weight: bold;
19 | font-size: 2rem;
20 | line-height: 22px;
21 | letter-spacing: 0.15px;
22 | color: rgba(0, 0, 0, 0.87);
23 | margin-bottom: 18px;
24 | }
25 |
26 | p {
27 | font-weight: 500;
28 | font-size: 1.8rem;
29 | line-height: 26px;
30 | display: flex;
31 | letter-spacing: -0.6px;
32 | color: #444241;
33 | display: flex;
34 | align-items: center;
35 | margin-bottom: 8px;
36 | img {
37 | margin-right: 6px;
38 | }
39 | }
40 | .desc {
41 | font-size: 1.6rem;
42 | line-height: 22px;
43 | letter-spacing: -0.6px;
44 | color: rgba(131, 131, 131, 0.87);
45 | margin-top: 14px;
46 | }
47 | a {
48 | font-size: 1.4rem;
49 | line-height: 18px;
50 | letter-spacing: -0.4px;
51 | text-decoration-line: underline;
52 | color: #ff6e35;
53 | }
54 | `
55 |
--------------------------------------------------------------------------------
/src/components/MyPage/Profile/index.tsx:
--------------------------------------------------------------------------------
1 | import { box } from './style'
2 | import React, { useCallback } from 'react'
3 | import { useRouter } from 'next/router'
4 |
5 | interface ProfileProps {
6 | id: string | null
7 | name: string | null
8 | job: string
9 | nowJob: string
10 | skill: string[]
11 | aboutme: string
12 | }
13 |
14 | const Profile = (props: ProfileProps) => {
15 | const myInfo = JSON.stringify(localStorage.getItem('access_token'))
16 | const router = useRouter()
17 |
18 | const UpdatePage = useCallback(() => {
19 | router.push('/sign_up/self_introduction/update')
20 | }, [router])
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
{props.name}
31 | {props.job}
32 |
33 |
34 | {myInfo ? (
35 |
36 |
37 |
38 | ) : (
39 |
40 | )}
41 |
42 |
43 |
44 |
{props.name}님은 현재?
45 |
{props.nowJob}이에요!
46 |
47 |
48 |
49 |
{props.name}님의 기술스택은?
50 |
51 | {props.skill.map((v, i) => (
52 | #{v}
53 | ))}
54 |
55 |
56 |
57 |
58 |
{props.name}님의 자기소개
59 |
60 | {props.aboutme}
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | export default Profile
68 |
--------------------------------------------------------------------------------
/src/components/MyPage/Profile/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const box = css`
4 | background: #ffffff;
5 | border: 1px solid #ffeee7;
6 | box-sizing: border-box;
7 | box-shadow: 0px 7px 24px rgb(0 0 0 / 10%);
8 | border-radius: 10px;
9 | margin-bottom: 18px;
10 | padding: 26px;
11 | .skill {
12 | margin-bottom: 2rem;
13 | ul {
14 | display: flex;
15 |
16 | li {
17 | margin-right: 5px;
18 | }
19 | }
20 | }
21 | .profile {
22 | display: grid;
23 | grid-template-columns: 1fr 6fr auto;
24 | align-items: center;
25 | margin-bottom: 15px;
26 | .me {
27 | margin-left: 1.2em;
28 | h2 {
29 | margin-bottom: 6px;
30 | }
31 | }
32 |
33 | p {
34 | width: 56px;
35 | height: 56px;
36 | border-radius: 50%;
37 | box-sizing: border-box;
38 | object-fit: cover;
39 | background-color: #d3cfcc;
40 | img {
41 | width: 56px;
42 | height: 56px;
43 | border-radius: 50%;
44 | box-sizing: border-box;
45 | object-fit: cover;
46 | }
47 | }
48 | strong {
49 | font-size: 1.6rem;
50 | line-height: 22px;
51 | letter-spacing: -0.6px;
52 | color: #ff6e35;
53 | }
54 | button {
55 | justify-content: end;
56 | }
57 | }
58 | h2 {
59 | font-weight: bold;
60 | font-size: 2rem;
61 | line-height: 22px;
62 | letter-spacing: 0.15px;
63 | color: rgba(0, 0, 0, 0.87);
64 | }
65 |
66 | .moreme {
67 | &.career {
68 | margin-bottom: 2rem;
69 | }
70 | h3 {
71 | font-size: 1.6rem;
72 | line-height: 20px;
73 | letter-spacing: -0.4px;
74 | color: #8f8c8b;
75 | margin-bottom: 8px;
76 | }
77 | p,
78 | li {
79 | font-weight: 500;
80 | font-size: 1.6rem;
81 | line-height: 24px;
82 | letter-spacing: -1px;
83 | color: #444241;
84 | }
85 | }
86 | `
87 |
--------------------------------------------------------------------------------
/src/components/MyPage/School/index.tsx:
--------------------------------------------------------------------------------
1 | import { box } from './style'
2 | import React, { useCallback } from 'react'
3 | import { useRouter } from 'next/router'
4 |
5 | interface SchoolProps {
6 | degree: string
7 | graduated: boolean
8 | major: string
9 | }
10 |
11 | const School = (props: SchoolProps) => {
12 | const myInfo = JSON.stringify(localStorage.getItem('access_token'))
13 | const router = useRouter()
14 |
15 | const UpdatePage = useCallback(() => {
16 | router.push('/sign_up/school/update')
17 | }, [router])
18 |
19 | return (
20 |
21 | 학교정보
22 |
23 | {/* 학위 / 전공 */}
24 | {props.degree} / {props.major}
25 |
26 | {/* 재학여부 */}
27 | {props.graduated === true ? 졸업
: 재학중
}
28 |
29 | {myInfo ? (
30 |
31 |
32 |
33 | ) : (
34 |
35 | )}
36 |
37 | )
38 | }
39 |
40 | export default School
41 |
--------------------------------------------------------------------------------
/src/components/MyPage/School/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const box = css`
4 | background: #ffffff;
5 | border: 1px solid #ffeee7;
6 | box-sizing: border-box;
7 | box-shadow: 0px 7px 24px rgb(0 0 0 / 10%);
8 | border-radius: 10px;
9 | margin-bottom: 18px;
10 | padding: 26px;
11 | position: relative;
12 | button {
13 | position: absolute;
14 | right: 26px;
15 | top: 26px;
16 | }
17 | h2 {
18 | font-weight: bold;
19 | font-size: 2rem;
20 | line-height: 22px;
21 | letter-spacing: 0.15px;
22 | color: rgba(0, 0, 0, 0.87);
23 | margin-bottom: 18px;
24 | }
25 |
26 | p {
27 | font-weight: 500;
28 | font-size: 1.8rem;
29 | line-height: 26px;
30 | display: flex;
31 | letter-spacing: -0.6px;
32 | color: #444241;
33 | display: flex;
34 | align-items: center;
35 | margin-bottom: 8px;
36 | }
37 | .desc {
38 | font-size: 1.6rem;
39 | line-height: 22px;
40 | letter-spacing: -0.6px;
41 | color: rgba(131, 131, 131, 0.87);
42 | margin-top: 3px;
43 | }
44 | `
45 |
--------------------------------------------------------------------------------
/src/components/PortFolioDeleteForm/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | interface IList {
4 | idx: number
5 | name: string
6 | link: string
7 | description?: string
8 | isDelete?: boolean
9 | onDelete: (idx: number) => void
10 | }
11 |
12 | function PortFolioDeleteForm(props: IList) {
13 | const handleDelete = () => {
14 | props.onDelete(props.idx)
15 | }
16 |
17 | return (
18 |
19 | {props.idx !== 0 && !props.isDelete && (
20 |
40 | )}
41 |
42 | )
43 | }
44 |
45 | export default PortFolioDeleteForm
46 |
47 | const info = css`
48 | background: #ffffff;
49 | border: 1px solid #ffeee7;
50 | box-sizing: border-box;
51 | box-shadow: 0px 7px 24px rgba(0, 0, 0, 0.1);
52 | border-radius: 10px;
53 | margin-bottom: 18px;
54 | padding: 26px;
55 | p {
56 | font-size: 2rem;
57 | margin-bottom: 22px;
58 | color: #444;
59 | &:nth-of-type(1) {
60 | margin-top: 44px;
61 | }
62 | b {
63 | font-weight: 600;
64 | }
65 | }
66 | `
67 |
68 | const infoWrap = css`
69 | padding: 0.5rem 0;
70 | .formBox {
71 | margin-bottom: 250px;
72 | }
73 | .delete {
74 | font-size: 30px;
75 | float: right;
76 | color: #444;
77 | }
78 | `
79 |
--------------------------------------------------------------------------------
/src/components/Post/EditComment/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 | import React, { useCallback, useEffect, useState } from 'react'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { RootState } from 'src/reducers'
5 | import { UPDATE_COMMENT_REQUEST } from 'src/reducers/comments'
6 | import { CLOSE_EDITMODE, CLOSE_ISMODAL } from 'src/reducers/common'
7 |
8 | export type Props = {
9 | value: string
10 | postId: string | string[] | undefined
11 | commentId: number
12 | }
13 |
14 | function EditComment({ value, commentId, postId }: Props) {
15 | const [text, setText] = useState(value)
16 | const dispatch = useDispatch()
17 | const { updateCommentSuccess } = useSelector((state: RootState) => state.comments)
18 | const router = useRouter()
19 |
20 | useEffect(() => {
21 | if (updateCommentSuccess) router.reload()
22 | }, [updateCommentSuccess, router])
23 |
24 | const onChangeText = useCallback((e) => {
25 | setText(e.target.value)
26 | }, [])
27 |
28 | const editComment = useCallback(
29 | (e) => {
30 | e.preventDefault()
31 | try {
32 | dispatch({
33 | type: UPDATE_COMMENT_REQUEST,
34 | data: {
35 | postId: Number(postId),
36 | commentId: commentId,
37 | text: text,
38 | },
39 | })
40 | } catch (err) {
41 | console.error(err)
42 | }
43 | },
44 | [dispatch, commentId, postId, text]
45 | )
46 |
47 | return (
48 |
68 | )
69 | }
70 |
71 | export default EditComment
72 |
--------------------------------------------------------------------------------
/src/components/SignupDeleteForm/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | interface IList {
4 | idx: number
5 | role: string
6 | technology: string
7 | years: number | string
8 | isDelete: boolean
9 | onDelete: (idx: number) => void
10 | }
11 |
12 | function SignupDeleteForm(props: IList) {
13 | const handleDelete = () => {
14 | props.onDelete(props.idx)
15 | }
16 |
17 | return (
18 |
19 | {props.idx !== 0 && !props.isDelete && (
20 |
40 | )}
41 |
42 | )
43 | }
44 |
45 | export default SignupDeleteForm
46 |
47 | const info = css`
48 | background: #ffffff;
49 | border: 1px solid #ffeee7;
50 | box-sizing: border-box;
51 | box-shadow: 0px 7px 24px rgba(0, 0, 0, 0.1);
52 | border-radius: 10px;
53 | margin-bottom: 18px;
54 | padding: 26px;
55 | p {
56 | font-size: 2rem;
57 | margin-bottom: 22px;
58 | color: #444;
59 | &:nth-of-type(1) {
60 | margin-top: 44px;
61 | }
62 | b {
63 | font-weight: 600;
64 | }
65 | }
66 | `
67 |
68 | const infoWrap = css`
69 | padding: 0.5rem 0;
70 | .formBox {
71 | margin-bottom: 250px;
72 | }
73 | .delete {
74 | font-size: 30px;
75 | float: right;
76 | color: #444;
77 | }
78 | `
79 |
--------------------------------------------------------------------------------
/src/components/SplashScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | // import Emo from 'public/images/splashScreen/logo01.svg'
3 | // import Dog from 'public/images/splashScreen/dog.svg'
4 |
5 | function SplashScreen() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | 웰시와 함께 쉬운 코딩
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default SplashScreen
25 |
26 | const screen = css`
27 | position: relative;
28 | section {
29 | padding: 0 20px;
30 | }
31 | .top {
32 | position: absolute;
33 | width: 100%;
34 | height: 100%;
35 | top: 9rem;
36 | left: 4rem;
37 | }
38 | h1 {
39 | font-weight: 500;
40 | font-size: 4rem;
41 | line-height: 52px;
42 | letter-spacing: -1px;
43 | color: #ffffff;
44 | text-align: left;
45 | margin-top: 1em;
46 | }
47 | .dog {
48 | position: absolute;
49 | top: 50vh;
50 | right: 5vw;
51 | }
52 | `
53 |
--------------------------------------------------------------------------------
/src/components/SubmitModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { Modal } from './styles'
2 |
3 | interface Props {
4 | prevLink: string
5 | setPrevLink: (e: any) => void
6 | onClose: () => void
7 | onSubmit: (e: any) => void
8 | setLink: (e: any) => void
9 | link: string
10 | h3: string
11 | p1: string
12 | p2?: string
13 | }
14 |
15 | const SubmitModal = ({ prevLink, setPrevLink, onClose, onSubmit, h3, p1, p2 }: Props) => {
16 | return (
17 |
18 |
19 |
20 |
21 |
{h3}
22 |
23 | {p1}
24 |
25 | {p2}
26 |
27 |
28 |
29 | setPrevLink(e.target.value)} />
30 |
31 |
32 |
33 |
34 | 취소
35 |
36 |
37 | 확인
38 |
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default SubmitModal
47 |
--------------------------------------------------------------------------------
/src/components/SubmitModal/styles.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const Modal = css`
4 | width: 100%;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | background: rgba(0, 0, 0, 0.3);
13 | z-index: 9999;
14 | .modal {
15 | &__submit {
16 | width: 100%;
17 | padding: 10px 20px;
18 |
19 | input {
20 | width: 80%;
21 | border-bottom: 1px solid #696766;
22 | padding: 10px 0;
23 | font-size: 1.5rem;
24 |
25 | @media (max-width: 420px) {
26 | width: 100%;
27 | }
28 | :focus {
29 | border-bottom: 1px solid #ff6e35;
30 | }
31 | }
32 | }
33 | &__wrap {
34 | max-width: 600px;
35 | width: 100%;
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | position: relative;
40 | img {
41 | position: absolute;
42 | top: -78px;
43 | z-index: 100;
44 | }
45 | }
46 | &__box {
47 | box-sizing: border;
48 | overflow: hidden;
49 | width: 77%;
50 | background: #ffffff;
51 | backdrop-filter: blur(12px);
52 | border-radius: 16px;
53 | text-align: center;
54 | h3 {
55 | font-weight: 700;
56 | font-size: 2.2rem;
57 | line-height: 26px;
58 | letter-spacing: -1px;
59 | color: #262626;
60 | padding-top: 33px;
61 |
62 | @media (max-width: 420px) {
63 | font-size: 1.6rem;
64 | }
65 | }
66 | p {
67 | font-size: 1.6rem;
68 | line-height: 22px;
69 | text-align: center;
70 | letter-spacing: -0.6px;
71 | color: #696766;
72 | padding: 10px 0 20px 0;
73 |
74 | @media (max-width: 420px) {
75 | font-size: 1.2rem;
76 | }
77 | }
78 | }
79 | &__btn {
80 | display: flex;
81 | justify-content: center;
82 | align-content: center;
83 | width: 100%;
84 | border-top: 1px solid #efebe8;
85 | button {
86 | padding: 18px 0;
87 | font-size: 2rem;
88 | width: 50%;
89 | box-sizing: border-box;
90 |
91 | @media (max-width: 420px) {
92 | font-size: 1.5rem;
93 | }
94 | &.delete {
95 | color: #ff6e35;
96 | border-left: 1px solid #efebe8;
97 | }
98 | }
99 | }
100 | }
101 | `
102 |
--------------------------------------------------------------------------------
/src/components/Together/Header/Search/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 | import { useCallback, useState } from 'react'
3 | import { TogetherHeaderSearch } from './style'
4 |
5 | type Props = {
6 | onAddKeyword: (string: string) => void
7 | }
8 |
9 | function TogetherSearchBar({ onAddKeyword }: Props) {
10 | const [searchValue, setSearchValue] = useState('')
11 |
12 | const router = useRouter()
13 |
14 | const onChangeSearch = useCallback((e) => {
15 | setSearchValue(e.target.value)
16 | }, [])
17 |
18 | const onSubmit = useCallback(
19 | (e) => {
20 | e.preventDefault()
21 | // 로컬 스토리지에 해당 searchValue를 저장해야 한다
22 | router.push(`/together/search_result/${searchValue}`)
23 | onAddKeyword(searchValue)
24 | setSearchValue('')
25 | },
26 | [searchValue, router, onAddKeyword]
27 | )
28 |
29 | return (
30 |
38 | )
39 | }
40 |
41 | export default TogetherSearchBar
42 |
--------------------------------------------------------------------------------
/src/components/Together/Header/Search/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const TogetherHeaderSearch = css`
4 | width: 100%;
5 | display: flex;
6 | justify-content: space-around;
7 | padding-top: 20px;
8 |
9 | form {
10 | width: 100%;
11 | padding-right: 20px;
12 | }
13 |
14 | input {
15 | width: 100%;
16 | border-bottom: 1.6px solid #ff6e35;
17 | height: 100%;
18 | font-weight: 500;
19 | font-size: 18px;
20 | background: url('/images/header/search.svg') no-repeat left center;
21 | padding-left: 23px;
22 | }
23 | `
24 |
--------------------------------------------------------------------------------
/src/components/Together/Header/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 |
4 | import Link from 'next/link'
5 | import { useRouter } from 'next/router'
6 | import { TogetherHeaderInput } from './style'
7 |
8 | export type Props = {
9 | optional?: boolean
10 | notis?: boolean
11 | }
12 |
13 | function TogetherHeader({ optional, notis }: Props) {
14 | const router = useRouter()
15 |
16 | return (
17 |
39 | )
40 | }
41 |
42 | export default TogetherHeader
43 |
--------------------------------------------------------------------------------
/src/components/Together/Header/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const TogetherHeaderInput = css`
4 | background: white;
5 | display: flex;
6 | justify-content: space-around;
7 | padding: 20px 5px !important;
8 |
9 | div {
10 | cursor: pointer;
11 | display: flex;
12 | width: 80%;
13 | border-bottom: 1.6px solid #ff6e35;
14 | align-items: center;
15 | font-weight: 500;
16 | font-size: 18px;
17 | background: url('/images/header/search.svg') no-repeat left center;
18 | padding-left: 23px;
19 |
20 | @media (max-width: 420px) {
21 | margin-left: 12px;
22 | }
23 |
24 | span {
25 | font-weight: 500;
26 | font-size: 18px;
27 | color: gray;
28 | }
29 | }
30 | `
31 |
--------------------------------------------------------------------------------
/src/components/Together/SearchBox/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { studyContentBox } from './style'
3 |
4 | type Props = {
5 | id: number
6 | listTitle: string
7 | hashTag: string[]
8 | }
9 | function StudyBox({ id, listTitle, hashTag }: Props) {
10 | return (
11 |
12 |
13 |
14 |
15 | {listTitle}
16 | {hashTag ? hashTag.map((v, i) => #{v}
) : }
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default StudyBox
25 |
--------------------------------------------------------------------------------
/src/components/Together/SearchBox/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { Common } from 'src/styles/common'
3 |
4 | export const studyContentBox = css`
5 | background: #ffffff;
6 | box-shadow: 0px 7px 24px rgba(0, 0, 0, 0.1);
7 | border-radius: 10px;
8 | padding: 16px;
9 | width: 100%;
10 | margin-bottom: 16px;
11 | &:last-of-type {
12 | margin-bottom: 5em;
13 | }
14 | h2 {
15 | font-weight: 500;
16 | font-size: ${Common.fontSize.fs22};
17 | color: ${Common.colors.black};
18 | line-height: 26px;
19 | letter-spacing: -0.6px;
20 | }
21 | h3 {
22 | font-weight: 500;
23 | font-size: ${Common.fontSize.fs18};
24 | color: ${Common.colors.black};
25 | line-height: 26px;
26 | letter-spacing: -0.6px;
27 | padding-bottom: 16px;
28 | }
29 | p {
30 | background: #ffeee7;
31 | border-radius: 56px;
32 | padding: 8px 12px;
33 | color: #ff6e35;
34 | display: inline-block;
35 | font-weight: bold;
36 | font-size: ${Common.fontSize.fs14};
37 | line-height: 20px;
38 | margin-right: 5px;
39 | margin-top: 5px;
40 | }
41 | `
42 |
--------------------------------------------------------------------------------
/src/components/Together/StudyBox/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useEffect, useState } from 'react'
3 | import { studyContentBox } from './style'
4 |
5 | type Props = {
6 | key: number
7 | uniq: number
8 | listTitle: string
9 | tags: []
10 | }
11 | function StudyBox({ uniq, listTitle, tags }: Props) {
12 | const [maxtags, setMaxtags] = useState(tags)
13 |
14 | useEffect(() => {
15 | if (tags.length >= 3) {
16 | setMaxtags(tags.slice(0, 4))
17 | }
18 | }, [tags])
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | {listTitle}
26 | {maxtags?.map((v, i) => (
27 | #{v}
28 | ))}
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default StudyBox
37 |
--------------------------------------------------------------------------------
/src/components/Together/StudyBox/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import { Common } from 'src/styles/common'
3 |
4 | export const studyContentBox = css`
5 | background: #ffffff;
6 | box-shadow: 0px 7px 24px rgba(0, 0, 0, 0.1);
7 | border-radius: 10px;
8 | padding: 16px;
9 | width: 74%;
10 | margin-bottom: 35px;
11 | flex: 0 0 auto;
12 | margin-left: 16px;
13 | &:last-of-type {
14 | margin-right: 16px;
15 | }
16 | h2 {
17 | font-weight: 500;
18 | font-size: ${Common.fontSize.fs22};
19 | color: ${Common.colors.black};
20 | line-height: 26px;
21 | letter-spacing: -0.6px;
22 | }
23 |
24 | h3 {
25 | font-weight: 500;
26 | font-size: ${Common.fontSize.fs18};
27 | color: ${Common.colors.black};
28 | line-height: 26px;
29 | letter-spacing: -0.6px;
30 | padding-bottom: 16px;
31 | }
32 | p {
33 | background: #ffeee7;
34 | border-radius: 56px;
35 | padding: 8px 12px;
36 | color: #ff6e35;
37 | display: inline-block;
38 | font-weight: bold;
39 | font-size: ${Common.fontSize.fs14};
40 | line-height: 20px;
41 | margin-right: 5px;
42 | margin-top: 5px;
43 | }
44 | `
45 |
--------------------------------------------------------------------------------
/src/components/Together/StudySection/index.tsx:
--------------------------------------------------------------------------------
1 | import StudySlider from 'src/components/Together/StudySlider'
2 | import { css } from '@emotion/react'
3 | import { PostData } from 'src/types'
4 |
5 | function StudySection({ theme, posts }: PostData) {
6 | return (
7 |
8 |
9 | # {theme} 모임이에요!
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default StudySection
18 |
19 | const titleStyle = css`
20 | font-size: 2.2rem;
21 | color: #262626;
22 | font-weight: 500;
23 | padding: 22px 0px 16px 20px;
24 | `
25 |
--------------------------------------------------------------------------------
/src/components/Together/StudySectionOption/index.tsx:
--------------------------------------------------------------------------------
1 | import StudySlider from 'src/components/Together/StudySlider'
2 | import { css } from '@emotion/react'
3 | import { PostType } from 'src/types'
4 |
5 | // data는 dataProps { } 객체 형식으로 이루어진 배열이다
6 |
7 | type Props = {
8 | theme: string
9 | posts?: PostType[]
10 | }
11 |
12 | function StudySectionOpt({ theme, posts }: Props) {
13 | return (
14 |
15 |
16 | {theme}모임
17 |
18 |
19 | {posts ? : }
20 |
21 | )
22 | }
23 |
24 | export default StudySectionOpt
25 |
26 | const titleStyle = css`
27 | font-size: 2.2rem;
28 | color: #262626;
29 | font-weight: 500;
30 | margin-top: 22px;
31 | margin-left: 20px;
32 | margin-bottom: 16px;
33 | `
34 |
--------------------------------------------------------------------------------
/src/components/Together/StudySlider/index.tsx:
--------------------------------------------------------------------------------
1 | import StudyBox from 'src/components/Together/StudyBox'
2 | import { PostType } from 'src/types'
3 | import { studyContentBox } from '../StudyBox/style'
4 | import { studyContentList, studyContentListWrap } from './style'
5 |
6 | type Props = {
7 | theme?: string
8 | data?: PostType[]
9 | }
10 |
11 | function StudySlider({ theme, data }: Props) {
12 | return (
13 |
30 | )
31 | }
32 |
33 | export default StudySlider
34 |
--------------------------------------------------------------------------------
/src/components/Together/StudySlider/style.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | export const studyContentListWrap = css`
4 | display: flex;
5 | position: relative;
6 | `
7 |
8 | export const studyContentList = css`
9 | overflow: hidden;
10 | overflow-x: scroll;
11 | width: 100%;
12 | margin-right: 0;
13 | flex-wrap: nowrap;
14 | display: flex;
15 | &::-webkit-scrollbar {
16 | display: none;
17 | }
18 | `
19 |
--------------------------------------------------------------------------------
/src/components/Together/WriteButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import Link from 'next/link'
3 |
4 | const WriteBtn = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default WriteBtn
19 |
20 | export const WriteButton = css`
21 | right: 0;
22 | bottom: 0;
23 | z-index: 10;
24 | position: absolute;
25 | bottom: 100px;
26 |
27 | @media (max-width: 420px) {
28 | bottom: 100px;
29 | }
30 |
31 | .writeWrap {
32 | height: 95%;
33 | display: grid;
34 | justify-items: end;
35 | width: 100%;
36 | margin: 0 auto;
37 | padding: 0 20px;
38 | .writeBtnWrap {
39 | width: 100%;
40 | text-align: end;
41 | }
42 | }
43 | `
44 |
--------------------------------------------------------------------------------
/src/hooks/useHandleOverflow.ts:
--------------------------------------------------------------------------------
1 | export default function usehandleOverFlow() {
2 | function hidden() {
3 | const x = document.getElementsByTagName('BODY')[0] as HTMLStyleElement
4 | x.style.overflow = 'hidden'
5 | }
6 |
7 | function show() {
8 | const x = document.getElementsByTagName('BODY')[0] as HTMLStyleElement
9 | x.style.overflow = 'auto'
10 | }
11 |
12 | function modalShow() {
13 | const x = document.getElementsByClassName('css-qxiuy7-togetherBoard')[0] as HTMLStyleElement
14 | x.style.height = '100%'
15 | x.style.overflow = 'scroll'
16 | }
17 |
18 | return { hidden, show, modalShow }
19 | }
20 |
21 | /*
22 | SearchModal이 모달 형식을 띄고 있기 때문에 반응형으로 넘어갈 시에 스크롤이 두 개가 생기는 현상을 발견하였습니다.
23 | 이를 컨트롤하기 위해서 최상위 태그인 body에 접근하여 overflow를 제어할 수 있도록 만들었습니다.
24 | */
25 |
--------------------------------------------------------------------------------
/src/hooks/useHeader.ts:
--------------------------------------------------------------------------------
1 | function useHeader() {
2 | const teamName = '🐶 MIC-TEAM (추가 수정해서 푸시하면 어떻게 될까?)'
3 |
4 | return { teamName }
5 | }
6 |
7 | export default useHeader
8 |
--------------------------------------------------------------------------------
/src/lib/apiClient.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance, AxiosResponse } from 'axios'
2 |
3 | export const apiClient: AxiosInstance = axios.create({
4 | baseURL: `${process.env.NEXT_PUBLIC_API_URL}`,
5 | timeout: 1000,
6 | })
7 |
8 | apiClient.defaults.headers.post['Content-Type'] = 'application/json'
9 | apiClient.defaults.withCredentials = true
10 |
11 | const responseFulfilled = (response: AxiosResponse) => {
12 | // 응답 데이터 가공
13 | return response
14 | }
15 |
16 | const responseRejected = (error: any) => {
17 | // 오류 응답 처리
18 | return Promise.reject(error)
19 | }
20 |
21 | apiClient.interceptors.response.use(responseFulfilled, responseRejected)
22 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import FooterMenu from 'src/components/Common/FooterMenu'
2 | import React from 'react'
3 | import TogetherBack from 'src/components/Common/Header/Back'
4 | import WellseeError from 'src/components/Common/wellseeError'
5 |
6 | const NotFound = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 |
14 | >
15 | )
16 | }
17 |
18 | export default NotFound
19 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import type { AppProps } from 'next/app'
3 | import wrapper from 'src/store'
4 | import Head from 'next/head'
5 | import { GlobalStyles } from 'src/styles/global-styles'
6 |
7 | function App({ Component, pageProps }: AppProps) {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 | {GlobalStyles}
15 |
16 |
17 |
18 | >
19 | )
20 | }
21 |
22 | export default wrapper.withRedux(App)
23 |
24 | const mainWrap = css`
25 | height: 100%;
26 | width: 100%;
27 | overflow-y: auto;
28 | `
29 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | class MyDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | export default MyDocument
21 |
--------------------------------------------------------------------------------
/src/pages/_error.tsx:
--------------------------------------------------------------------------------
1 | import FooterMenu from 'src/components/Common/FooterMenu'
2 | import React from 'react'
3 | import TogetherBack from 'src/components/Common/Header/Back'
4 |
5 | const Error = () => {
6 | return (
7 | <>
8 |
9 |
10 |
21 |
22 |
23 |
24 | 에러가 발생했어요
25 |
26 |
27 |
28 |
29 |
30 | >
31 | )
32 | }
33 |
34 | export default Error
35 |
--------------------------------------------------------------------------------
/src/pages/alarm/index.tsx:
--------------------------------------------------------------------------------
1 | import AlarmList from 'src/components/Alarm/List'
2 | import AlarmTitle from 'src/components/Alarm/Title'
3 | import Back from 'src/components/Common/Header/Back'
4 | import { useCallback, useEffect, useState } from 'react'
5 | import Head from 'next/head'
6 | import { useDispatch, useSelector } from 'react-redux'
7 | import { RootState } from 'src/reducers'
8 | import { FETCHING_NOTIS_REQUEST } from 'src/reducers/notifications'
9 | import { RESET_POST_LIST } from 'src/reducers/posts'
10 | import axios from 'axios'
11 |
12 | const Alarm = () => {
13 | const { notifications } = useSelector((state: RootState) => state.notifications)
14 | const { post } = useSelector((state: RootState) => state.posts)
15 |
16 | /* 읽지 않음 알림의 개수 */
17 | const [alarmCnt, setAlarmCnt] = useState(0)
18 |
19 | const dispatch = useDispatch()
20 |
21 | useEffect(() => {
22 | if (typeof window !== 'undefined') {
23 | axios.defaults.headers.common = {
24 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
25 | }
26 | }
27 | }, [])
28 |
29 | useEffect(() => {
30 | if (post.length)
31 | dispatch({
32 | type: RESET_POST_LIST,
33 | })
34 | }, [post, dispatch])
35 |
36 | useEffect(() => {
37 | notifications.length && countNotReadedAlarm()
38 | }, [notifications])
39 |
40 | useEffect(() => {
41 | if (!notifications.length) fetchNotifications()
42 | }, [])
43 |
44 | const fetchNotifications = useCallback(() => {
45 | dispatch({
46 | type: FETCHING_NOTIS_REQUEST,
47 | })
48 | }, [dispatch])
49 |
50 | const countNotReadedAlarm = useCallback(() => {
51 | let cnt = 0
52 | notifications.forEach((v) => {
53 | if (v.read === false) cnt++
54 | })
55 | setAlarmCnt(cnt)
56 | }, [notifications])
57 |
58 | return (
59 |
60 |
61 |
알림 | wellseecoding
62 |
63 |
64 |
65 |
66 |
67 |
68 | )
69 | }
70 |
71 | export default Alarm
72 |
--------------------------------------------------------------------------------
/src/pages/failure/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Failure = () => {
4 | return (
5 |
6 |
Failure 실패했어요
7 |
8 | )
9 | }
10 |
11 | export default Failure
12 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import axios from 'axios'
3 | import FooterMenu from 'src/components/Common/FooterMenu'
4 | import SplashScreen from 'src/components/SplashScreen'
5 | import { useEffect } from 'react'
6 |
7 | import Head from 'next/head'
8 | import router from 'next/router'
9 |
10 | function Home() {
11 | // 1차적으로 복호화했을 때 아직 이름이 없어서, 해당 정보를 어떻게 저장할 지는 협의해봐야 할 것 같네요
12 | // ① 환경 변수에 등록한 토큰을 디코딩
13 | useEffect(() => {
14 | if (typeof window !== 'undefined') {
15 | if (localStorage.getItem('access_token')?.length) {
16 | axios.defaults.headers.common = {
17 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
18 | }
19 | } else {
20 | setTimeout(() => {
21 | checkDecode()
22 | })
23 | }
24 | }
25 | }, [])
26 |
27 | /* 복호화된 토큰 정보가 없을 경우 회원가입 단으로 이동 */
28 | function checkDecode() {
29 | if (!localStorage.getItem('id')) {
30 | router.push('/sign_in/auth_start')
31 | }
32 | }
33 |
34 | return (
35 | <>
36 |
37 | 웰시코딩 | wellseecoding
38 |
39 |
40 |
41 |
42 |
43 |
44 | >
45 | )
46 | }
47 |
48 | export default Home
49 |
50 | const container = css`
51 | width: 100%;
52 | height: 100%;
53 | background: #ff6e35;
54 | `
55 |
--------------------------------------------------------------------------------
/src/pages/mypage/index.tsx:
--------------------------------------------------------------------------------
1 | import FooterMenu from 'src/components/Common/FooterMenu'
2 | import Profile from 'src/components/MyPage/Profile'
3 | import School from 'src/components/MyPage/School'
4 | import { css } from '@emotion/react'
5 | import Portfolio from 'src/components/MyPage/Portfolio'
6 | import Career from 'src/components/MyPage/Career'
7 | import Head from 'next/head'
8 | import { useDispatch, useSelector } from 'react-redux'
9 | import { RootState } from 'src/reducers'
10 | import { useEffect, useState } from 'react'
11 | import { FETCHING_MYPAGE_REQUEST } from 'src/reducers/mypage'
12 | import axios from 'axios'
13 | import LogOutModal from 'src/components/LogOutModal'
14 | import Loading from 'src/components/Loading'
15 |
16 | const MyPage = () => {
17 | const { myPages } = useSelector((state: RootState) => state.mypage)
18 | /* 로컬 스토리지에서 가져온 사용자 이름 */
19 | const [name, setName] = useState('')
20 | /* 로컬 스토리지에서 가져온 사용자 id */
21 | const [id, setId] = useState('')
22 | /* 로컬 스토리지에서 토큰을 꺼낸뒤 실행하기 위한 블로킹 처리 */
23 | const [tokenState, setTokenState] = useState(false)
24 | const [isShow, setIsShow] = useState(false)
25 | const dispatch = useDispatch()
26 |
27 | useEffect(() => {
28 | if (typeof window !== 'undefined') {
29 | /* 토큰 꺼내기 */
30 | axios.defaults.headers.common = {
31 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
32 | }
33 | /* 이름 설정하기 */
34 | setName(localStorage.getItem('userName'))
35 | setId(localStorage.getItem('userId'))
36 | /* 정상처리 된다면 token 상태 true로 바꾸기 */
37 | setTokenState(true)
38 | }
39 | }, [])
40 |
41 | useEffect(() => {
42 | /* myPage가 빈 배열이고, 토큰상태가 충족될 때 request 보내기 */
43 | if (!myPages.length && tokenState) {
44 | dispatch({
45 | type: FETCHING_MYPAGE_REQUEST,
46 | })
47 | }
48 | }, [dispatch, tokenState])
49 |
50 | const logOutModal = () => {
51 | setIsShow(true)
52 | }
53 | const logOutModalClose = () => {
54 | setIsShow(false)
55 | }
56 | return (
57 | <>
58 |
59 | 마이 페이지 | wellseecoding
60 |
61 |
62 |
63 | {myPages.length ? (
64 | myPages.map((v, i) => (
65 |
66 |
67 |
68 | {v.educations.map((v: any, i: number) => (
69 |
70 |
71 |
72 | ))}
73 |
74 | {v.links.map((link: any, i: number) => (
75 |
78 | ))}
79 |
80 | {v.works.map((v: any, i: number) => (
81 |
82 |
83 |
84 | ))}
85 |
86 |
91 |
92 | ))
93 | ) : (
94 |
95 | )}
96 |
97 | {isShow ? : null}
98 |
99 |
100 | >
101 | )
102 | }
103 | export default MyPage
104 | const mypageWrap = css`
105 | margin-bottom: 100px;
106 | .logout {
107 | width: 100%;
108 | text-align: center;
109 | padding-top: 16px;
110 | button {
111 | display: inline-flex;
112 | font-weight: 500;
113 | font-size: 18px;
114 | line-height: 26px;
115 | letter-spacing: -0.6px;
116 | color: #d3cfcc;
117 | }
118 | }
119 | `
120 | const profilePadding = css`
121 | padding: 40px 20px 0;
122 | position: relative;
123 | &::before {
124 | position: absolute;
125 | content: '';
126 | background: #ff6e35;
127 | height: 200px;
128 | width: 100%;
129 | z-index: -100;
130 | left: 0;
131 | top: 0;
132 | }
133 | `
134 | const moreWrap = css`
135 | padding: 0 20px;
136 | `
137 |
--------------------------------------------------------------------------------
/src/pages/sign_in/auth_start/index.tsx:
--------------------------------------------------------------------------------
1 | import AuthLogin from 'src/components/AuthLogin'
2 | import Back from 'src/components/Common/Header/Back'
3 | import { css } from '@emotion/react'
4 | import { Common } from 'src/styles/common'
5 | import Head from 'next/head'
6 | // import Logo from '/public/images/login/character_color.svg'
7 |
8 | const AuthLoginStart = () => {
9 | return (
10 | <>
11 |
12 | 로그인 | wellseecoding
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 웰시와 함께
22 | 쉬운 스터디
23 |
24 |
25 |
26 |
27 | >
28 | )
29 | }
30 |
31 | export default AuthLoginStart
32 |
33 | const authLoginTitleWrap = css`
34 | padding: 0 20px;
35 | padding-top: 5rem;
36 | `
37 | const bigTitle = css`
38 | font-size: ${Common.fontSize.bigTitle};
39 | font-weight: 500;
40 | color: ${Common.colors.black};
41 | margin-top: 27px;
42 | line-height: 52px;
43 | `
44 |
--------------------------------------------------------------------------------
/src/pages/sign_in/email_start/index.tsx:
--------------------------------------------------------------------------------
1 | import TextField from 'src/components/Common/TextField'
2 | import React, { useCallback, useState } from 'react'
3 | import PasswordField from 'src/components/Common/PasswordField'
4 | import BigTitle from 'src/components/Common/BigTitle'
5 | import { css } from '@emotion/react'
6 | import { useRouter } from 'next/router'
7 | import FootButton, { FootButtonType } from 'src/components/Common/FootButton'
8 | import Back from 'src/components/Common/Header/Back'
9 | import axios from 'axios'
10 |
11 | const EmailLogin = () => {
12 | const router = useRouter()
13 |
14 | // 이메일, 비밀번호
15 | const [email, setEmail] = useState('')
16 | const [password, setPassword] = useState('')
17 |
18 | //오류메시지 상태저장
19 | const [emailMessage, setEmailMessage] = useState('')
20 | const [passwordMessage, setPasswordMessage] = useState('')
21 |
22 | // 유효성 검사
23 | const [isEmail, setIsEmail] = useState(false)
24 | const [isPassword, setIsPassword] = useState(false)
25 |
26 | const onSubmit = useCallback(
27 | async (e: React.FormEvent) => {
28 | e.preventDefault()
29 |
30 | try {
31 | await axios
32 | .post('/api/v1/users/token', {
33 | email: email,
34 | password: password,
35 | })
36 | .then((res) => {
37 | if (res.status === 200) {
38 | router.push('/token')
39 | }
40 | })
41 | } catch (err) {
42 | console.error(err)
43 | }
44 | },
45 | [password, email, router]
46 | )
47 |
48 | // 이메일
49 | const onChangeEmail = useCallback((e: React.ChangeEvent) => {
50 | const emailRegex =
51 | /([\w-.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/
52 | const emailCurrent = e.target.value
53 | setEmail(emailCurrent)
54 |
55 | if (!emailRegex.test(emailCurrent)) {
56 | setEmailMessage('이메일 형식이 틀렸어요! 다시 확인해주세요 ㅜ ㅜ')
57 | setIsEmail(false)
58 | } else {
59 | setEmailMessage('올바른 이메일 형식이에요 : )')
60 | setIsEmail(true)
61 | }
62 | }, [])
63 |
64 | // 비밀번호
65 | const onChangePassword = useCallback((e: React.ChangeEvent) => {
66 | const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$/
67 | const passwordCurrent = e.target.value
68 | setPassword(passwordCurrent)
69 |
70 | if (!passwordRegex.test(passwordCurrent)) {
71 | setPasswordMessage('숫자+영문자+특수문자 조합으로 8자리 이상 입력해주세요!')
72 | setIsPassword(false)
73 | } else {
74 | setPasswordMessage('안전한 비밀번호에요 : )')
75 | setIsPassword(true)
76 | }
77 | }, [])
78 |
79 | return (
80 | <>
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | {email.length > 0 && {emailMessage} }
92 |
93 |
94 |
95 |
101 | {password.length > 0 && (
102 |
{passwordMessage}
103 | )}
104 |
105 |
106 |
107 | 다음
108 |
109 |
110 |
111 |
112 | >
113 | )
114 | }
115 |
116 | export default EmailLogin
117 |
118 | const loginFrom = css`
119 | margin-top: 1.7em;
120 | padding: 0 20px;
121 | .formbox {
122 | position: relative;
123 | margin-bottom: 20px;
124 | .message {
125 | font-weight: 500;
126 | font-size: 1.6rem;
127 | line-height: 24px;
128 | letter-spacing: -1px;
129 | position: absolute;
130 | bottom: -10px;
131 | left: 0;
132 | &.success {
133 | color: #8f8c8b;
134 | }
135 | &.error {
136 | color: #ff2727;
137 | }
138 | }
139 | }
140 | `
141 |
142 | const loginTitleWrap = css`
143 | padding: 0 20px;
144 | `
145 |
146 | const loginAlign = css`
147 | padding-top: 60px;
148 | display: flex;
149 | justify-content: center;
150 | align-content: center;
151 | flex-direction: column;
152 | height: 90%;
153 | `
154 |
155 | const footButtonWrapper = css`
156 | margin-top: 68px;
157 | button:disabled,
158 | button[disabled] {
159 | background-color: #d3cfcc;
160 | color: #ffffff;
161 | }
162 | `
163 |
--------------------------------------------------------------------------------
/src/pages/sign_in/find_password/index.tsx:
--------------------------------------------------------------------------------
1 | import Back from 'src/components/Common/Header/Back'
2 | import { TextField } from '@material-ui/core'
3 | import FootButton, { FootButtonType } from 'src/components/Common/FootButton'
4 | import Title from 'src/components/Common/Title'
5 | import { css } from '@emotion/react'
6 |
7 | const PasswordEmailSubmit = () => {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 다음
22 |
23 |
24 |
25 | >
26 | )
27 | }
28 |
29 | export default PasswordEmailSubmit
30 |
31 | const passwordResetForm = css`
32 | margin-top: 1.7em;
33 | padding: 0 20px;
34 | `
35 |
36 | const footButtonWrapper = css`
37 | position: absolute;
38 | bottom: 4.4em;
39 | left: 0;
40 | right: 0;
41 | padding: 0 20px;
42 | `
43 |
--------------------------------------------------------------------------------
/src/pages/sign_in/reset_password/index.tsx:
--------------------------------------------------------------------------------
1 | import FootButton, { FootButtonType } from 'src/components/Common/FootButton'
2 | import Back from 'src/components/Common/Header/Back'
3 | import PasswordField from 'src/components/Common/PasswordField'
4 | import Title from 'src/components/Common/Title'
5 | import { css } from '@emotion/react'
6 | import { useRouter } from 'next/router'
7 | import React, { useCallback, useState } from 'react'
8 |
9 | interface PasswordReset {
10 | password: string
11 | }
12 |
13 | const PasswordReset = () => {
14 | const router = useRouter()
15 | //비밀번호, 비밀번호 확인
16 | const [password, setPassword] = useState('')
17 | const [passwordConfirm, setPasswordConfirm] = useState('')
18 |
19 | //오류메시지 상태저장
20 | const [passwordMessage, setPasswordMessage] = useState('')
21 | const [passwordConfirmMessage, setPasswordConfirmMessage] = useState('')
22 |
23 | // 유효성 검사
24 | const [isPassword, setIsPassword] = useState(false)
25 | const [isPasswordConfirm, setIsPasswordConfirm] = useState(false)
26 |
27 | const onSubmit = useCallback(
28 | async (e: React.FormEvent) => {
29 | e.preventDefault()
30 | router.push('/home')
31 | },
32 | [router]
33 | )
34 |
35 | // 비밀번호
36 | const onChangePassword = useCallback((e: React.ChangeEvent) => {
37 | const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$/
38 | const passwordCurrent = e.target.value
39 | setPassword(passwordCurrent)
40 |
41 | if (!passwordRegex.test(passwordCurrent)) {
42 | setPasswordMessage('숫자+영문자+특수문자 조합으로 8자리 이상 입력해주세요!')
43 | setIsPassword(false)
44 | } else {
45 | setPasswordMessage('안전한 비밀번호에요 : )')
46 | setIsPassword(true)
47 | }
48 | }, [])
49 |
50 | // 비밀번호 확인
51 | const onChangePasswordConfirm = useCallback(
52 | (e: React.ChangeEvent) => {
53 | const passwordConfirmCurrent = e.target.value
54 | setPasswordConfirm(passwordConfirmCurrent)
55 |
56 | if (password === passwordConfirmCurrent) {
57 | setPasswordConfirmMessage('비밀번호를 똑같이 입력했어요 : )')
58 | setIsPasswordConfirm(true)
59 | } else {
60 | setPasswordConfirmMessage('비밀번호가 틀려요. 다시 확인해주세요 ㅜ ㅜ')
61 | setIsPasswordConfirm(false)
62 | }
63 | },
64 | [password]
65 | )
66 |
67 | return (
68 | <>
69 |
70 |
71 |
72 |
73 |
74 |
75 |
81 | {password.length > 0 && (
82 |
{passwordMessage}
83 | )}
84 |
85 |
86 |
87 |
93 | {passwordConfirm.length > 0 && (
94 |
{passwordConfirmMessage}
95 | )}
96 |
97 |
98 |
99 |
104 | 다음
105 |
106 |
107 |
108 | >
109 | )
110 | }
111 |
112 | export default PasswordReset
113 |
114 | const passwordForm = css`
115 | margin-top: 1.7em;
116 | padding: 0 20px;
117 | .formbox {
118 | position: relative;
119 | margin-bottom: 20px;
120 | .message {
121 | font-weight: 500;
122 | font-size: 1.6rem;
123 | line-height: 24px;
124 | letter-spacing: -1px;
125 | position: absolute;
126 | bottom: -10px;
127 | left: 0;
128 | &.success {
129 | color: #8f8c8b;
130 | }
131 | &.error {
132 | color: #ff2727;
133 | }
134 | }
135 | }
136 | `
137 |
138 | const footButtonWrapper = css`
139 | position: absolute;
140 | bottom: 4.4em;
141 | left: 0;
142 | right: 0;
143 | padding: 0 20px;
144 |
145 | button:disabled,
146 | button[disabled] {
147 | background-color: #d3cfcc;
148 | color: #ffffff;
149 | }
150 | `
151 |
--------------------------------------------------------------------------------
/src/pages/sign_up/completion/index.tsx:
--------------------------------------------------------------------------------
1 | import Back from 'src/components/Common/Header/Back'
2 | import FootButton, { FootButtonType } from 'src/components/Common/FootButton'
3 | import { css } from '@emotion/react'
4 | import { Common } from 'src/styles/common'
5 | import { useRouter } from 'next/router'
6 |
7 | const SignUpCompletion = () => {
8 | const router = useRouter()
9 |
10 | const NextHome = () => {
11 | router.push('/home')
12 | }
13 |
14 | return (
15 | <>
16 |
17 |
18 |
19 | 프로필을 모두
20 | 완성했어요~!
21 |
22 |
23 |
24 |
25 |
26 | 시작하기
27 |
28 |
29 | >
30 | )
31 | }
32 |
33 | export default SignUpCompletion
34 |
35 | const profileCompletion = css`
36 | text-align: center;
37 | padding-top: 16vh;
38 | h2 {
39 | font-size: ${Common.fontSize.title};
40 | line-height: 31px;
41 | }
42 | img {
43 | margin-top: 4.2em;
44 | }
45 | `
46 |
47 | const footButtonWrapper = css`
48 | position: absolute;
49 | bottom: 4.4em;
50 | left: 0;
51 | right: 0;
52 | padding: 0 20px;
53 | `
54 |
--------------------------------------------------------------------------------
/src/pages/sign_up/experience/update.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useEffect } from 'react'
2 | import NeedUpdated from './need_update'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { FETCHING_MYPAGE_REQUEST } from 'src/reducers/mypage'
5 | import { RootState } from 'src/reducers'
6 | import axios from 'axios'
7 | import Loading from 'src/components/Loading'
8 | import Head from 'next/head'
9 |
10 | function ExperienceUpdate() {
11 | const { myPages } = useSelector((state: RootState) => state.mypage)
12 |
13 | const dispatch = useDispatch()
14 | const [tokenState, setTokenState] = useState(false)
15 | /*
16 | ② 로컬에 저장된 토큰을 꺼내서 default header로 설정한다
17 | 왜냐하면 env.local 에 저장된 토큰이 없다고 가정하고 진행하기 때문에
18 | 로컬스토리지에 저장한 엑세스 토큰을 꺼내서 초기 헤더 값으로 설정해주는 것이다
19 | */
20 | useEffect(() => {
21 | if (typeof window !== 'undefined') {
22 | /* 토큰 꺼내기 */
23 | axios.defaults.headers.common = {
24 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
25 | }
26 | /* 정상처리 된다면 token 상태 true로 바꾸기 */
27 | setTokenState(true)
28 | }
29 | }, [])
30 |
31 | /* ③ myPages가 없고, tokenState이 준비가 되었다면 정보를 불러온다 */
32 | useEffect(() => {
33 | if (!myPages.length && tokenState) {
34 | /* ④ 액션 디스패치 */
35 | fetchInfo()
36 | }
37 | }, [tokenState])
38 |
39 | useEffect(() => {
40 | if (typeof window !== 'undefined') {
41 | axios.defaults.headers.common = {
42 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
43 | }
44 | }
45 | }, [])
46 |
47 | /* ⑤ 이 액션을 통해 myPages 내부의 데이터가 들어온다 */
48 | const fetchInfo = useCallback(() => {
49 | dispatch({
50 | type: FETCHING_MYPAGE_REQUEST,
51 | })
52 | }, [dispatch])
53 |
54 | return (
55 |
56 |
57 |
경력 정보를 적어주세요
58 |
59 |
60 |
61 | {/*
62 | ⑦ myPages에 데이터가 존재할 경우, 이를 매핑하여 준다
63 | intialState의 값을 바로 하위 컴포넌트
에 props로 전달한다
64 | map 한 데이터는 readOnly 값으로 현 단계에서 수정할 수 없기 때문이다
65 | */}
66 | {myPages.length ? (
67 | myPages.map((v, i) => (
68 |
69 | {v.works.map((v: any, i: number) => (
70 |
71 | ))}
72 |
73 | ))
74 | ) : (
75 |
76 | )}
77 |
78 |
79 | )
80 | }
81 |
82 | export default ExperienceUpdate
83 |
--------------------------------------------------------------------------------
/src/pages/sign_up/portfolio/update.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react'
2 | import { FETCHING_MYPAGE_REQUEST } from 'src/reducers/mypage'
3 | import NeedUpdated from './need_update'
4 | import Title from 'src/components/Common/Title'
5 | import Back from 'src/components/Common/Header/Back'
6 | import Loading from 'src/components/Loading'
7 | import axios from 'axios'
8 | import { useDispatch, useSelector } from 'react-redux'
9 | import { RootState } from 'src/reducers'
10 | import Head from 'next/head'
11 |
12 | const PortfolioUpdate = () => {
13 | const { myPages } = useSelector((state: RootState) => state.mypage)
14 |
15 | const dispatch = useDispatch()
16 | const [tokenState, setTokenState] = useState(false)
17 |
18 | /*
19 | ② 로컬에 저장된 토큰을 꺼내서 default header로 설정한다
20 | 왜냐하면 env.local 에 저장된 토큰이 없다고 가정하고 진행하기 때문에
21 | 로컬스토리지에 저장한 엑세스 토큰을 꺼내서 초기 헤더 값으로 설정해주는 것이다
22 | */
23 | useEffect(() => {
24 | if (typeof window !== 'undefined') {
25 | /* 토큰 꺼내기 */
26 | axios.defaults.headers.common = {
27 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
28 | }
29 | /* 정상처리 된다면 token 상태 true로 바꾸기 */
30 | setTokenState(true)
31 | }
32 | }, [])
33 |
34 | /* ③ myPages가 없고, tokenState이 준비가 되었다면 정보를 불러온다 */
35 | useEffect(() => {
36 | if (!myPages.length && tokenState) {
37 | /* ④ 액션 디스패치 */
38 | fetchInfo()
39 | }
40 | }, [tokenState])
41 |
42 | useEffect(() => {
43 | if (typeof window !== 'undefined') {
44 | axios.defaults.headers.common = {
45 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
46 | }
47 | }
48 | }, [])
49 |
50 | /* ⑤ 이 액션을 통해 myPages 내부의 데이터가 들어온다 */
51 | const fetchInfo = useCallback(() => {
52 | dispatch({
53 | type: FETCHING_MYPAGE_REQUEST,
54 | })
55 | }, [dispatch])
56 |
57 | return (
58 | <>
59 |
60 | 포트폴리오를 올려주세요
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | {/*
70 | ⑦ myPages에 데이터가 존재할 경우, 이를 매핑하여 준다
71 | intialState의 값을 바로 하위 컴포넌트
에 props로 전달한다
72 | map 한 데이터는 readOnly 값으로 현 단계에서 수정할 수 없기 때문이다
73 | */}
74 | {myPages.length ? (
75 | myPages.map((v, i) => (
76 |
77 | {v.links.map((v: any, i: number) => (
78 |
79 | ))}
80 |
81 | ))
82 | ) : (
83 |
84 | )}
85 |
86 | >
87 | )
88 | }
89 |
90 | export default PortfolioUpdate
91 |
--------------------------------------------------------------------------------
/src/pages/sign_up/profile_start/index.tsx:
--------------------------------------------------------------------------------
1 | import Back from 'src/components/Common/Header/Back'
2 | import FootButton, { FootButtonType } from 'src/components/Common/FootButton'
3 | import { css } from '@emotion/react'
4 | import { Common } from 'src/styles/common'
5 | import { useRouter } from 'next/router'
6 |
7 | const SignUpProfileStart = () => {
8 | const router = useRouter()
9 |
10 | const NextHome = () => {
11 | router.push('/home')
12 | }
13 |
14 | const NextPage = () => {
15 | router.push('/sign_up/something_job')
16 | }
17 |
18 | return (
19 | <>
20 |
21 |
22 |
23 | 자, 이제 프로필을
24 | 만들어봐요~!
25 |
26 |
27 |
28 |
29 |
30 | 아 귀찮아요. 나중에 할래요
31 |
32 |
33 | 네~
34 |
35 |
36 | >
37 | )
38 | }
39 |
40 | export default SignUpProfileStart
41 |
42 | const profileStart = css`
43 | text-align: center;
44 | height: 100vh;
45 | justify-content: center;
46 | align-content: center;
47 | display: grid;
48 | text-align: center;
49 | place-items: center;
50 | h2 {
51 | font-size: ${Common.fontSize.title};
52 | line-height: 31px;
53 | margin-bottom: 1rem;
54 | }
55 | `
56 |
57 | const footButtonWrapper = css`
58 | position: absolute;
59 | bottom: 4.4em;
60 | left: 0;
61 | right: 0;
62 | padding: 0 20px;
63 |
64 | & > button:nth-of-type(1) {
65 | margin-bottom: 12px;
66 | }
67 | `
68 |
--------------------------------------------------------------------------------
/src/pages/sign_up/school/update.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react'
2 | import { FETCHING_MYPAGE_REQUEST } from 'src/reducers/mypage'
3 | import NeedUpdated from './need_update'
4 | import { RootState } from 'src/reducers'
5 | import Head from 'next/head'
6 | import axios from 'axios'
7 | import Loading from 'src/components/Loading'
8 | import { useDispatch, useSelector } from 'react-redux'
9 |
10 | const SelfIntroductionUpdate = () => {
11 | /* ① 초기 initialState로 설정된 객체 myPages를 불러온다 */
12 | const { myPages } = useSelector((state: RootState) => state.mypage)
13 | const dispatch = useDispatch()
14 |
15 | /* 로컬 스토리지에서 토큰을 꺼낸뒤 실행하기 위한 블로킹 처리 */
16 | const [tokenState, setTokenState] = useState(false)
17 |
18 | useEffect(() => {
19 | if (typeof window !== 'undefined') {
20 | /* 토큰 꺼내기 */
21 | axios.defaults.headers.common = {
22 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
23 | }
24 | setTokenState(true)
25 | }
26 | }, [])
27 |
28 | const fetchInfo = useCallback(() => {
29 | dispatch({
30 | type: FETCHING_MYPAGE_REQUEST,
31 | })
32 | }, [dispatch])
33 |
34 | /* ③ myPages가 없고, tokenState이 준비가 되었다면 정보를 불러온다 */
35 | useEffect(() => {
36 | if (!myPages.length && tokenState) {
37 | /* ④ 액션 디스패치 */
38 | fetchInfo()
39 | }
40 | }, [tokenState])
41 |
42 | return (
43 | <>
44 |
45 | 학교 정보를 적어주세요
46 |
47 |
48 |
49 | {/*
50 | ⑦ myPages에 데이터가 존재할 경우, 이를 매핑하여 준다
51 | intialState의 값을 바로 하위 컴포넌트
에 props로 전달한다
52 | map 한 데이터는 readOnly 값으로 현 단계에서 수정할 수 없기 때문이다
53 | */}
54 | {myPages.length ? (
55 | myPages.map((v, i) => (
56 |
57 | {v.educations.map((v: any, i: number) => (
58 |
59 |
60 |
61 | ))}
62 |
63 | ))
64 | ) : (
65 |
66 | )}
67 |
68 | >
69 | )
70 | }
71 |
72 | export default SelfIntroductionUpdate
73 |
--------------------------------------------------------------------------------
/src/pages/sign_up/self_introduction/update.tsx:
--------------------------------------------------------------------------------
1 | import Back from 'src/components/Common/Header/Back'
2 | import Title from 'src/components/Common/Title'
3 | import { useCallback, useEffect, useState } from 'react'
4 | import Head from 'next/head'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import { RootState } from 'src/reducers'
7 | import { FETCHING_MYPAGE_REQUEST } from 'src/reducers/mypage'
8 | import NeedUpdated from './need_update'
9 | import axios from 'axios'
10 | import Loading from 'src/components/Loading'
11 |
12 | const SelfIntroductionUpdate = () => {
13 | /* ① 초기 initialState로 설정된 객체 myPages를 불러온다 */
14 | const { myPages } = useSelector((state: RootState) => state.mypage)
15 |
16 | const dispatch = useDispatch()
17 |
18 | /* 로컬 스토리지에서 토큰을 꺼낸뒤 실행하기 위한 블로킹 처리 */
19 | const [tokenState, setTokenState] = useState(false)
20 |
21 | /*
22 | ② 로컬에 저장된 토큰을 꺼내서 default header로 설정한다
23 | 왜냐하면 env.local 에 저장된 토큰이 없다고 가정하고 진행하기 때문에
24 | 로컬스토리지에 저장한 엑세스 토큰을 꺼내서 초기 헤더 값으로 설정해주는 것이다
25 | */
26 | useEffect(() => {
27 | if (typeof window !== 'undefined') {
28 | /* 토큰 꺼내기 */
29 | axios.defaults.headers.common = {
30 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
31 | }
32 | /* 정상처리 된다면 token 상태 true로 바꾸기 */
33 | setTokenState(true)
34 | }
35 | }, [])
36 |
37 | /* ③ myPages가 없고, tokenState이 준비가 되었다면 정보를 불러온다 */
38 | useEffect(() => {
39 | if (!myPages.length && tokenState) {
40 | /* ④ 액션 디스패치 */
41 | fetchInfo()
42 | }
43 | }, [tokenState])
44 |
45 | useEffect(() => {
46 | preventEnterEvent()
47 | }, [])
48 |
49 | /* ⑤ 이 액션을 통해 myPages 내부의 데이터가 들어온다 */
50 | const fetchInfo = useCallback(() => {
51 | dispatch({
52 | type: FETCHING_MYPAGE_REQUEST,
53 | })
54 | }, [dispatch])
55 |
56 | const preventEnterEvent = () => {
57 | if (process.browser) {
58 | const inputs = document.querySelectorAll('input')
59 | inputs.forEach((input) => {
60 | input.addEventListener('keydown', (e: KeyboardEvent) => {
61 | if (e.key === 'Enter') {
62 | e.preventDefault()
63 | return false
64 | }
65 | })
66 | })
67 | }
68 | }
69 |
70 | return (
71 | <>
72 |
73 | 자기소개 해주세요!
74 |
75 |
76 |
77 |
78 |
79 | {/*
80 | ⑦ myPages에 데이터가 존재할 경우, 이를 매핑하여 준다
81 | intialState의 값을 바로 하위 컴포넌트
에 props로 전달한다
82 | map 한 데이터는 readOnly 값으로 현 단계에서 수정할 수 없기 때문이다
83 | */}
84 | {myPages.length ? (
85 | myPages.map((v, i) => (
86 |
87 |
88 |
89 |
90 | ))
91 | ) : (
92 |
93 | )}
94 |
95 | >
96 | )
97 | }
98 |
99 | export default SelfIntroductionUpdate
100 |
--------------------------------------------------------------------------------
/src/pages/sign_up/something_job/index.tsx:
--------------------------------------------------------------------------------
1 | import FootButton, { FootButtonType } from 'src/components/Common/FootButton'
2 | import Back from 'src/components/Common/Header/Back'
3 | import Title from 'src/components/Common/Title'
4 | import { css } from '@emotion/react'
5 | import { Common } from 'src/styles/common'
6 | import { useRouter } from 'next/router'
7 | import { useState, useCallback, useEffect } from 'react'
8 | import axios from 'axios'
9 | import { REGISTER_STATUS_URL } from 'src/apis'
10 | import Head from 'next/head'
11 |
12 | const SomethingJob = () => {
13 | const router = useRouter()
14 | // 직업 선택시 다음으로 넘어갈 수 있도록
15 | const [isChecked, setIsChecked] = useState(false)
16 |
17 | // 현재 직업
18 | const [value, setValue] = useState('')
19 |
20 | useEffect(() => {
21 | if (typeof window !== 'undefined') {
22 | axios.defaults.headers.common = {
23 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
24 | }
25 | }
26 | }, [])
27 |
28 | // 학생
29 | const onChangeStudent = useCallback(() => {
30 | const student = document.getElementsByClassName('student')
31 | const jobArr = student
32 |
33 | if (jobArr) {
34 | setValue('학생')
35 | setIsChecked(true)
36 | }
37 | }, [])
38 |
39 | // 직장인
40 | const onChangeWorker = useCallback(() => {
41 | const worker = document.getElementsByClassName('worker')
42 | const jobArr = worker
43 |
44 | if (jobArr) {
45 | setValue('직장인')
46 | setIsChecked(true)
47 | }
48 | }, [])
49 |
50 | // 취준생
51 | const onChangeJobSeeker = useCallback(() => {
52 | const jobSeeker = document.getElementsByClassName('jobSeeker')
53 | const jobArr = jobSeeker
54 |
55 | if (jobArr) {
56 | setValue('취준생')
57 | setIsChecked(true)
58 | }
59 | }, [])
60 |
61 | const onSubmit = useCallback(
62 | async (e: React.FormEvent) => {
63 | e.preventDefault()
64 | try {
65 | await axios
66 | .put(REGISTER_STATUS_URL, {
67 | status: value,
68 | })
69 | .then((res) => {
70 | if (res.status === 200) {
71 | router.push('/sign_up/self_introduction')
72 | }
73 | })
74 | } catch (err) {
75 | console.error(err)
76 | }
77 | },
78 | [value, router]
79 | )
80 |
81 | return (
82 | <>
83 |
84 | 어떤 일을 하고 계세요?
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | 학생
96 |
97 |
98 |
99 |
100 |
101 |
102 | 직장인
103 |
104 |
105 |
106 |
107 | 취준생
108 |
109 |
110 |
111 |
112 |
113 |
114 | 다음
115 |
116 |
117 |
118 |
119 | >
120 | )
121 | }
122 |
123 | export default SomethingJob
124 |
125 | const footButtonWrapper = css`
126 | position: fixed;
127 | bottom: 4rem;
128 | left: 0;
129 | right: 0;
130 | padding: 0 20px;
131 | button:disabled,
132 | button[disabled] {
133 | background-color: #d3cfcc;
134 | color: #ffffff;
135 | }
136 | .wrap {
137 | width: 100%;
138 | max-width: 550px;
139 | margin: 0 auto;
140 | & > button:nth-of-type(1) {
141 | margin-bottom: 11px;
142 | margin-top: 20px;
143 | }
144 | }
145 | `
146 |
147 | const job = css`
148 | text-align: center;
149 | display: grid;
150 | margin-top: 36px;
151 | grid-gap: 10px;
152 | justify-content: center;
153 | align-content: center;
154 | .row-coloum {
155 | grid-template-columns: auto auto;
156 | grid-gap: 10px;
157 | display: grid;
158 | }
159 | .job {
160 | border: 2px solid #efebe8;
161 | box-sizing: border-box;
162 | width: 163px;
163 | height: 163px;
164 | border-radius: 50%;
165 | display: inline-block;
166 | font-weight: 500;
167 | font-size: ${Common.fontSize.fs20};
168 | line-height: 28px;
169 | color: #b6b2b0;
170 | &:focus {
171 | border: 2px solid #ff6e35;
172 | color: #ff6e35;
173 | }
174 | }
175 | `
176 |
--------------------------------------------------------------------------------
/src/pages/template/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react'
2 | import DataForm from 'src/components/DataForm'
3 | import { css } from '@emotion/react'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import { RootState } from 'src/reducers'
6 | import { FETCHING_TODOS_REQUEST } from 'src/reducers/todos'
7 |
8 | function Template() {
9 | const dispatch = useDispatch()
10 | const { todos } = useSelector((state: RootState) => state.todos)
11 | // 배열의 첫 번째 인덱스
12 | const [firstNum, setFirstNum] = useState(0)
13 | // 배열의 마지막 번째 인덱스
14 | const [lastNum, setLastNum] = useState(20)
15 |
16 | const getAPI = () => {
17 | dispatch({
18 | type: FETCHING_TODOS_REQUEST,
19 | data: {
20 | first: firstNum,
21 | last: lastNum,
22 | },
23 | })
24 | updateNumber()
25 | }
26 |
27 | // useState로 받아오는 배열의 인덱스 추가
28 | const updateNumber = useCallback(() => {
29 | setFirstNum((num) => num + 20)
30 | setLastNum((num) => num + 20)
31 | }, [])
32 |
33 | return (
34 |
35 |
HELLO MIC!
36 |
37 |
38 |
39 | Click Me!
40 |
41 |
42 | )
43 | }
44 |
45 | export default Template
46 |
47 | export const h1Style = css`
48 | color: hotpink;
49 | font-size: 7rem;
50 | transition: 500ms;
51 | text-align: center;
52 | margin-top: 50px;
53 |
54 | &:hover {
55 | background-color: hotpink;
56 | color: white;
57 | }
58 | `
59 |
60 | export const btnStyle = css`
61 | cursor: pointer;
62 | font-size: 4rem;
63 | padding: 10px;
64 | border: none;
65 | background-color: hotpink;
66 | color: white;
67 | `
68 |
--------------------------------------------------------------------------------
/src/pages/together/index.tsx:
--------------------------------------------------------------------------------
1 | import FooterMenu from 'src/components/Common/FooterMenu'
2 | import TogetherHeader from 'src/components/Together/Header'
3 | import StudySection from 'src/components/Together/StudySection'
4 | import WriteButton from 'src/components/Together/WriteButton'
5 | import { css } from '@emotion/react'
6 | import { useDispatch, useSelector } from 'react-redux'
7 | import { RootState } from 'src/reducers'
8 | import { useCallback, useEffect, useState } from 'react'
9 | import { FETCHING_POSTS_REQUEST, RESET_POST_LIST } from 'src/reducers/posts'
10 | import Loading from 'src/components/Loading'
11 | import Head from 'next/head'
12 | import { notificationType } from 'src/types'
13 | import { FETCHING_NOTIS_REQUEST } from 'src/reducers/notifications'
14 | import axios from 'axios'
15 |
16 | const Write = () => {
17 | const { posts, post } = useSelector((state: RootState) => state.posts)
18 | const { notifications } = useSelector((state: RootState) => state.notifications)
19 | const dispatch = useDispatch()
20 |
21 | /* 알림 유무를 판단할 state */
22 | const [notis, setNotis] = useState(false)
23 |
24 | useEffect(() => {
25 | if (typeof window !== 'undefined') {
26 | axios.defaults.headers.common = {
27 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
28 | }
29 | }
30 | }, [])
31 |
32 | /* 알림 배열이 빈 배열일 경우 서버에 요청하여 알림 배열을 불러온다 */
33 | useEffect(() => {
34 | if (!notifications.length) fetchNotifications()
35 | }, [])
36 |
37 | /* 알림 배열이 넘어왔을 경우 읽지 않은 알림이 있는지 확인하는 로직을 실행 */
38 | useEffect(() => {
39 | if (notifications.length) {
40 | checkNotificationss(notifications)
41 | }
42 | }, [notifications])
43 |
44 | useEffect(() => {
45 | !posts.length && loadUser()
46 | }, [posts])
47 |
48 | useEffect(() => {
49 | post.length && resetPost()
50 | }, [post])
51 |
52 | const loadUser = useCallback(() => {
53 | dispatch({
54 | type: FETCHING_POSTS_REQUEST,
55 | })
56 | }, [dispatch])
57 |
58 | const resetPost = useCallback(() => {
59 | dispatch({
60 | type: RESET_POST_LIST,
61 | })
62 | }, [dispatch])
63 |
64 | const fetchNotifications = useCallback(() => {
65 | dispatch({
66 | type: FETCHING_NOTIS_REQUEST,
67 | })
68 | }, [dispatch])
69 |
70 | const checkNotificationss = useCallback((arr: notificationType[]) => {
71 | arr.forEach((v) => {
72 | if (v.read === false) setNotis(true)
73 | })
74 | }, [])
75 |
76 | return (
77 |
78 |
79 |
함께해요 | wellseecoding
80 |
81 |
82 |
83 |
84 |
85 |
86 | {posts ? posts.map((p, i) => ) : }
87 |
88 |
89 |
90 |
91 |
92 |
93 | )
94 | }
95 |
96 | export default Write
97 |
98 | const togetherBoard = css`
99 | background: #ffeee7;
100 | width: 100%;
101 | height: 85vh;
102 | padding-bottom: 100px;
103 |
104 | @media (max-width: 420px) {
105 | height: 90vh;
106 | background: #ffeee7;
107 | }
108 |
109 | .wrap {
110 | height: 100%;
111 | padding-top: 8px;
112 | background: #ffeee7;
113 | }
114 |
115 | .wrap section:last-child {
116 | background: #ffeee7;
117 | @media (max-width: 420px) {
118 | padding-bottom: 100px;
119 | }
120 | }
121 | `
122 |
--------------------------------------------------------------------------------
/src/pages/together/search/index.tsx:
--------------------------------------------------------------------------------
1 | import TogetherSearchBar from 'src/components/Together/Header/Search'
2 | import { css } from '@emotion/react'
3 | import { useEffect, useState } from 'react'
4 | import Link from 'next/link'
5 | import Head from 'next/head'
6 | import { useDispatch, useSelector } from 'react-redux'
7 | import { RootState } from 'src/reducers'
8 | import { RESET_SEARCH_LIST } from 'src/reducers/posts'
9 |
10 | interface keyInterface {
11 | id: number
12 | text: string
13 | }
14 |
15 | const Search = () => {
16 | const [keywords, setKeywords] = useState([])
17 | const dispatch = useDispatch()
18 |
19 | const { searchPosts } = useSelector((state: RootState) => state.posts)
20 |
21 | useEffect(() => {
22 | if (searchPosts.length) {
23 | dispatch({ type: RESET_SEARCH_LIST })
24 | }
25 | }, [searchPosts, dispatch])
26 |
27 | // ① window 즉, 브라우저가 모두 렌더링된 상태에서 해당 함수를 실행할 수 있도록 작업
28 | useEffect(() => {
29 | if (typeof window !== 'undefined') {
30 | const result = localStorage.getItem('keywords') || '[]'
31 | setKeywords(JSON.parse(result))
32 | }
33 | }, [])
34 |
35 | // ② keywords 객체를 의존하여, 변경될 경우 새롭게 localStroage의 아이템 'keywords'를 세팅한다
36 | useEffect(() => {
37 | localStorage.setItem('keywords', JSON.stringify(keywords))
38 | }, [keywords])
39 |
40 | // 검색어 추가
41 | const handleAddKeyword = (text: string) => {
42 | const newKeyword = {
43 | id: Date.now(),
44 | text: text,
45 | }
46 | setKeywords([newKeyword, ...keywords])
47 | }
48 |
49 | // 단일 검색어 삭제
50 | const handleRemoveKeyword = (id: number) => {
51 | const nextKeyword = keywords.filter((keyword) => {
52 | return keyword.id != id
53 | })
54 | setKeywords(nextKeyword)
55 | }
56 |
57 | //검색어 전체 삭제
58 | const handleClearKeywords = () => {
59 | setKeywords([])
60 | }
61 |
62 | return (
63 | <>
64 |
65 | 검색하기 | wellseecoding
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
최근 검색어
75 | {keywords.length ? (
76 |
77 | 전체 삭제
78 |
79 | ) : (
80 |
81 | )}
82 |
83 |
84 |
102 |
103 |
104 | >
105 | )
106 | }
107 |
108 | export default Search
109 |
110 | const searchRecord = css`
111 | display: flex;
112 | justify-content: space-between;
113 | width: 100%;
114 | margin-top: 39px;
115 | margin-bottom: 31px;
116 | h2 {
117 | font-weight: 500;
118 | font-size: 20px;
119 | line-height: 24px;
120 | letter-spacing: -1px;
121 | color: #262626;
122 | }
123 | button {
124 | font-weight: 500;
125 | font-size: 16px;
126 | line-height: 24px;
127 | letter-spacing: -1px;
128 | color: #d3cfcc;
129 | }
130 | `
131 |
132 | const searchList = css`
133 | width: 100%;
134 | li {
135 | display: flex;
136 | justify-content: space-between;
137 | margin-bottom: 25px;
138 | p {
139 | font-weight: 500;
140 | font-size: 16px;
141 | line-height: 24px;
142 | letter-spacing: -1px;
143 | color: #444241;
144 | }
145 | button {
146 | width: 10%;
147 | img {
148 | float: right;
149 | }
150 | }
151 | }
152 | `
153 | const searchWrap = css`
154 | height: 97vh;
155 | `
156 |
--------------------------------------------------------------------------------
/src/pages/together/search_result/[id].tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 | import TogetherHeader from 'src/components/Together/Header'
3 | import SearchBox from 'src/components/Together/SearchBox'
4 | import { useRouter } from 'next/router'
5 | import React, { useCallback, useEffect } from 'react'
6 | import { useDispatch, useSelector } from 'react-redux'
7 | import { RootState } from 'src/reducers'
8 | import { RESET_POST_LIST, SEARCH_POSTS_REQUEST } from 'src/reducers/posts'
9 | import Head from 'next/head'
10 | import axios from 'axios'
11 |
12 | const SearchResult = () => {
13 | const router = useRouter()
14 | const { id } = router.query
15 |
16 | const dispatch = useDispatch()
17 |
18 | const { searchPosts, post } = useSelector((state: RootState) => state.posts)
19 |
20 | useEffect(() => {
21 | if (typeof window !== 'undefined') {
22 | axios.defaults.headers.common = {
23 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
24 | }
25 | }
26 | }, [])
27 |
28 | useEffect(() => {
29 | if (post.length) {
30 | dispatch({
31 | type: RESET_POST_LIST,
32 | })
33 | }
34 | }, [post, dispatch])
35 |
36 | useEffect(() => {
37 | // 검색 데이터 잘못 보내고 있는 부분 수정함
38 | if (typeof id === 'string' && !searchPosts.length) {
39 | searchKeyword(id)
40 | }
41 | }, [id])
42 |
43 | const searchKeyword = useCallback(
44 | (id) => {
45 | dispatch({
46 | type: SEARCH_POSTS_REQUEST,
47 | data: id,
48 | })
49 | },
50 | [dispatch]
51 | )
52 |
53 | return (
54 | <>
55 |
56 | 검색 결과 | wellseecoding
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {id} 을(를) 검색한 결과입니다.
65 |
66 |
67 |
68 |
69 | {searchPosts.length ? (
70 | searchPosts.map((item) => (
71 |
72 | ))
73 | ) : (
74 | 검색한 결과가 없습니다.
75 | )}
76 |
77 |
78 |
79 | >
80 | )
81 | }
82 |
83 | export default SearchResult
84 |
85 | const searchWord = css`
86 | font-weight: 500;
87 | line-height: 24px;
88 | letter-spacing: -1px;
89 | font-size: 1.6rem;
90 | color: #262626;
91 | margin-top: 34px;
92 | h2 {
93 | margin-bottom: 20px;
94 | }
95 | strong {
96 | font-size: 2rem;
97 | font-weight: 700;
98 | }
99 | `
100 |
101 | const searchWrap = css`
102 | height: 97vh;
103 | overflow-y: auto;
104 | `
105 |
--------------------------------------------------------------------------------
/src/pages/token/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable prettier/prettier */
2 | import axios from 'axios'
3 | import React, { useCallback, useEffect, useState } from 'react'
4 | import { useRouter } from 'next/router'
5 | import WellseeError from 'src/components/Common/wellseeError'
6 |
7 | const Token = () => {
8 | const [response, setResponse] = useState('')
9 |
10 | const [tokenId, setTokenId] = useState('')
11 | const [decodedUserId, setDecodedUserId] = useState('')
12 | const [decodedUserName, setDecodedUserName] = useState('')
13 |
14 | const [likes, setLikes] = useState([])
15 | const [registeredGroup, setRegisteredGroup] = useState([])
16 |
17 | const [needInfo, setNeedInfo] = useState(false)
18 |
19 | const [ready, setReady] = useState(false)
20 |
21 | const router = useRouter()
22 |
23 | /* ① document.cookie 스토리지에서 전달 받은 access_token을 분해한다 */
24 | useEffect(() => {
25 | if (typeof window !== 'undefined') {
26 | splitToken(document.cookie)
27 | }
28 | }, [])
29 |
30 | /* ② access_token의 value가 정상적으로 response state안에 저장되었을 경우, 해당 토큰을 복호화한다 */
31 | useEffect(() => {
32 | if (response.length) parseJwt(response)
33 | }, [response])
34 |
35 | /* ③ 토큰 id가 정상적으로 생성되었다면 payload에서 사용자 정보를 추출한다 */
36 | useEffect(() => {
37 | if (tokenId) {
38 | checkId(tokenId)
39 | }
40 | }, [tokenId])
41 |
42 | /* ④ 마지막 복호화된 토큰의 userId 를 로컬 스토리지에 저장한다 */
43 | useEffect(() => {
44 | if (decodedUserId) localStorage.setItem('id', decodedUserId)
45 | if (decodedUserName) localStorage.setItem('userName', decodedUserName)
46 | }, [decodedUserId, decodedUserName])
47 |
48 | // ⑤ 토큰이 있다면 좋아요한 게시물이 있는지 요청을 보낸다
49 | useEffect(() => {
50 | if (typeof window !== 'undefined' && response.length) {
51 | axios.defaults.headers.common = {
52 | Authorization: `Bearer ` + localStorage.getItem('access_token'),
53 | }
54 | Promise.allSettled([getUserInfo(), getLikesGroup(), getRegisteredGroup()]).then((res) => {
55 | if (res[0].status === 'fulfilled' && res[1].status === 'fulfilled' && res[2].status === 'fulfilled') {
56 | // 흐름 ⓼ 로 이동
57 | setReady(true)
58 | } else {
59 | console.error('error')
60 | }
61 | })
62 | }
63 | }, [response, router, needInfo])
64 |
65 | // ⑥ API 요청을 통해 얻은 정보를 state에 저장한 후, 로컬 스토리지에 저장
66 | useEffect(() => {
67 | if (likes.length) {
68 | localStorage.setItem('myLikes', JSON.stringify(likes))
69 | }
70 | }, [likes])
71 |
72 | // ⑦ API 요청을 통해 얻은 정보를 state에 저장한 후, 로컬 스토리지에 저장
73 | useEffect(() => {
74 | if (registeredGroup.length) {
75 | localStorage.setItem('registered', JSON.stringify(registeredGroup))
76 | }
77 | }, [registeredGroup])
78 |
79 | // ⓼ 모든 HTTP request가 settled된 상태에서 유저 정보가 없다면 ① 회원가입 페이지 있다면 ② 메인페이지로 이동
80 | useEffect(() => {
81 | if (ready) {
82 | if (needInfo) {
83 | // ① 회원 가입 페이지
84 | router.push('/sign_up/profile_start')
85 | } else {
86 | // ② 메인 페이지
87 | router.push('/home')
88 | }
89 | }
90 | }, [ready, needInfo, router])
91 |
92 | /* 토큰을 분해해서 response state에 저장하는 함수 */
93 | const splitToken = (token: string) => {
94 | if (token) {
95 | // 흐름 ② 로 이동
96 | setResponse(token.replace('access_token=', ''))
97 | localStorage.setItem('access_token', token.replace('access_token=', ''))
98 | } else {
99 | console.error('쿠키에 저장된 토큰이 없습니다')
100 | return false
101 | }
102 | }
103 |
104 | /* JWT 토큰을 디코딩(복호화)한다. */
105 | const parseJwt = (token: string) => {
106 | try {
107 | const base64Url = token.split('.')[1]
108 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
109 | const jsonPayload = decodeURIComponent(
110 | atob(base64)
111 | .split('')
112 | .map(function (c) {
113 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
114 | })
115 | .join('')
116 | )
117 | // ③ 흐름 3으로 이동
118 | return setTokenId(JSON.parse(jsonPayload))
119 | } catch (e) {
120 | return null
121 | }
122 | }
123 |
124 | /* 복호화된 토큰 중 userId 정보를 분리한다. */
125 | const checkId = (tokenId: any) => {
126 | // 객체를 순회할 때는 for in문을 사용한다.
127 | for (const key in tokenId) {
128 | if (key === 'sub') {
129 | // 흐름 ④ 로 이동
130 | setDecodedUserId(tokenId[key])
131 | }
132 | if (key === 'uname') {
133 | // 흐름 ④ 로 이동
134 | setDecodedUserName(tokenId[key])
135 | }
136 | }
137 | }
138 | /* 토큰을 바탕으로 유저 정보를 확인하는 함수 */
139 | const getUserInfo = useCallback(async () => {
140 | await axios.get('/api/v1/users/profile').then((res: any) => {
141 | if (res.data.status === null) {
142 | setNeedInfo(true)
143 | }
144 | })
145 | }, [])
146 | /* 토큰을 바탕으로 좋아요 한 게시글을 불러오는 함수 */
147 | const getLikesGroup = useCallback(async () => {
148 | try {
149 | await axios.get('/api/v1/users/likes').then((res) => {
150 | setLikes(res.data.likes)
151 | })
152 | } catch (err) {
153 | console.error(err)
154 | }
155 | }, [])
156 | /* 토큰을 바탕으로 가입된 게시글을 불러오는 함수 */
157 | const getRegisteredGroup = useCallback(async () => {
158 | try {
159 | await axios.get('/api/v1/users/groups/registered').then((res) => setRegisteredGroup(res.data.groups))
160 | } catch (err) {
161 | console.error(err)
162 | }
163 | }, [])
164 |
165 | return (
166 |
167 |
168 |
169 | )
170 | }
171 |
172 | export default Token
173 |
--------------------------------------------------------------------------------
/src/reducers/common/index.tsx:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 |
3 | export interface CommonState {
4 | isModal: {
5 | open: boolean
6 | uniqId: string
7 | }
8 | editMode: boolean
9 | }
10 |
11 | // initialState 정의
12 | export const initialState: CommonState = {
13 | isModal: {
14 | open: false,
15 | uniqId: '',
16 | },
17 | editMode: false,
18 | }
19 |
20 | // 액션 정의
21 | export const OPEN_ISMODAL = 'OPEN_ISMODAL' as const
22 | export const OPEN_EDITMODE = 'OPEN_EDITMODE' as const
23 | export const CLOSE_EDITMODE = 'CLOSE_EDITMODE' as const
24 | export const CLOSE_ISMODAL = 'CLOSE_ISMODAL' as const
25 |
26 | // 액션에 대한 타입 정의;
27 | export interface OpenIsModal {
28 | type: typeof OPEN_ISMODAL
29 | data: string
30 | }
31 |
32 | export interface OpenEditMode {
33 | type: typeof OPEN_EDITMODE
34 | }
35 |
36 | export interface CloseEditMode {
37 | type: typeof CLOSE_EDITMODE
38 | }
39 |
40 | export interface CloseIsModal {
41 | type: typeof CLOSE_ISMODAL
42 | }
43 |
44 | // 리듀서 안에 들어갈 액션 타입에 대한 액션 생성 함수 정의
45 |
46 | export const setIsModal = (data: string): OpenIsModal => ({
47 | type: OPEN_ISMODAL,
48 | data,
49 | })
50 |
51 | export const setEditMode = (): OpenEditMode => ({
52 | type: OPEN_EDITMODE,
53 | })
54 |
55 | export const closeEditMode = (): CloseEditMode => ({
56 | type: CLOSE_EDITMODE,
57 | })
58 |
59 | export const closeIsModal = (): CloseIsModal => ({
60 | type: CLOSE_ISMODAL,
61 | })
62 |
63 | export type SetCommon =
64 | | ReturnType
65 | | ReturnType
66 | | ReturnType
67 | | ReturnType
68 |
69 | const common = (state = initialState, action: SetCommon) =>
70 | produce(state, (draft) => {
71 | switch (action.type) {
72 | case OPEN_ISMODAL: {
73 | draft.isModal.open = true
74 | draft.isModal.uniqId = action.data
75 | break
76 | }
77 | case OPEN_EDITMODE: {
78 | draft.editMode = true
79 | break
80 | }
81 | case CLOSE_EDITMODE: {
82 | draft.editMode = false
83 | break
84 | }
85 | case CLOSE_ISMODAL: {
86 | draft.isModal.open = false
87 | break
88 | }
89 |
90 | default:
91 | return state
92 | }
93 | })
94 |
95 | export default common
96 |
--------------------------------------------------------------------------------
/src/reducers/home/index.tsx:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 | import { homeData } from 'src/types'
3 |
4 | export interface HomeInitialState {
5 | homePosts: homeData[]
6 |
7 | fetchHomePostsLoading: boolean
8 | fetchHomePostsSuccess: boolean
9 | fetchHomePostsFailure: null | Error
10 | }
11 |
12 | export const initialState: HomeInitialState = {
13 | homePosts: [],
14 |
15 | fetchHomePostsLoading: false,
16 | fetchHomePostsSuccess: false,
17 | fetchHomePostsFailure: null,
18 | }
19 |
20 | // 액션 정의
21 | export const FETCHING_HOME_POSTS_REQUEST = 'FETCHING_HOME_POSTS_REQUEST' as const
22 | export const FETCHING_HOME_POSTS_SUCCESS = 'FETCHING_HOME_POSTS_SUCCESS' as const
23 | export const FETCHING_HOME_POSTS_FAILURE = 'FETCHING_HOME_POSTS_FAILURE' as const
24 |
25 | // 액션에 대한 타입 정의;
26 | export interface FetchingHomePostsRequest {
27 | type: typeof FETCHING_HOME_POSTS_REQUEST
28 | }
29 |
30 | export interface FetchingHomePostsSuccess {
31 | type: typeof FETCHING_HOME_POSTS_SUCCESS
32 | homePosts: homeData
33 | data: []
34 | }
35 |
36 | export interface FetchingHomePostsFailure {
37 | type: typeof FETCHING_HOME_POSTS_FAILURE
38 | error: Error
39 | }
40 |
41 | // 리듀서 안에 들어갈 액션 타입에 대한 액션 생성 함수 정의
42 |
43 | export const fetchingHomePostsRequest = (): FetchingHomePostsRequest => ({
44 | type: FETCHING_HOME_POSTS_REQUEST,
45 | })
46 |
47 | export const fetchingHomePostsSuccess = (homePosts: homeData, data: []): FetchingHomePostsSuccess => ({
48 | type: FETCHING_HOME_POSTS_SUCCESS,
49 | homePosts,
50 | data,
51 | })
52 |
53 | export const fetchingHomePostsFailure = (error: Error): FetchingHomePostsFailure => ({
54 | type: FETCHING_HOME_POSTS_FAILURE,
55 | error,
56 | })
57 |
58 | export type HomeActions =
59 | | ReturnType
60 | | ReturnType
61 | | ReturnType
62 |
63 | const home = (state: HomeInitialState = initialState, action: HomeActions) =>
64 | produce(state, (draft) => {
65 | switch (action.type) {
66 | case FETCHING_HOME_POSTS_REQUEST: {
67 | draft.fetchHomePostsLoading = true
68 | draft.fetchHomePostsSuccess = false
69 | break
70 | }
71 | case FETCHING_HOME_POSTS_SUCCESS: {
72 | draft.fetchHomePostsLoading = false
73 | draft.fetchHomePostsSuccess = true
74 | draft.homePosts = draft.homePosts.concat(action.data)
75 | break
76 | }
77 | case FETCHING_HOME_POSTS_FAILURE: {
78 | draft.fetchHomePostsSuccess = false
79 | draft.fetchHomePostsFailure = action.error
80 | break
81 | }
82 | default:
83 | return state
84 | }
85 | })
86 |
87 | export default home
88 |
--------------------------------------------------------------------------------
/src/reducers/index.tsx:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 | import posts from './posts'
4 | import comments from './comments'
5 | import common from './common'
6 | import home from './home'
7 | import notifications from './notifications'
8 | import mypage from './mypage'
9 |
10 | const rootReducer = combineReducers({
11 | todos,
12 | posts,
13 | common,
14 | comments,
15 | home,
16 | notifications,
17 | mypage,
18 | })
19 |
20 | export default rootReducer
21 |
22 | export type RootState = ReturnType
23 |
--------------------------------------------------------------------------------
/src/reducers/todos/index.tsx:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 | import { TodoType } from 'src/types'
3 |
4 | // initialState 타입 정의
5 | export interface TodosIntialState {
6 | todos: TodoType[]
7 |
8 | fetchTodosLoading: boolean
9 | fetchTodosSuccess: boolean
10 | fetchTodosFailure: null | Error
11 | }
12 |
13 | // initialState 정의
14 | export const initialState: TodosIntialState = {
15 | todos: [],
16 |
17 | fetchTodosLoading: false,
18 | fetchTodosSuccess: false,
19 | fetchTodosFailure: null,
20 | }
21 |
22 | // 액션 정의
23 | export const FETCHING_TODOS_REQUEST = 'FETCHING_TODOS_REQUEST' as const
24 | export const FETCHING_TODOS_SUCCESS = 'FETCHING_TODOS_SUCCESS' as const
25 | export const FETCHING_TODOS_FAILURE = 'FETCHING_TODOS_FAILURE' as const
26 |
27 | // 액션에 대한 타입 정의;
28 | export interface FetchingTodosRequest {
29 | type: typeof FETCHING_TODOS_REQUEST
30 | data: {
31 | first: number
32 | last: number
33 | }
34 | }
35 |
36 | export interface FetchingTodosSuccess {
37 | type: typeof FETCHING_TODOS_SUCCESS
38 | todos: TodoType
39 | data: []
40 | }
41 |
42 | export interface FetchingTodosFailure {
43 | type: typeof FETCHING_TODOS_FAILURE
44 | error: Error
45 | }
46 |
47 | // 리듀서 안에 들어갈 액션 타입에 대한 액션 생성 함수 정의
48 |
49 | export const fetchingToddsRequest = (data: { first: number; last: number }): FetchingTodosRequest => ({
50 | type: FETCHING_TODOS_REQUEST,
51 | data,
52 | })
53 |
54 | export const fetchingToddsSuccess = (todos: TodoType, data: []): FetchingTodosSuccess => ({
55 | type: FETCHING_TODOS_SUCCESS,
56 | todos,
57 | data,
58 | })
59 |
60 | export const fetchingToddsFailure = (error: Error): FetchingTodosFailure => ({
61 | type: FETCHING_TODOS_FAILURE,
62 | error,
63 | })
64 |
65 | export type FetchingTodos =
66 | | ReturnType
67 | | ReturnType
68 | | ReturnType
69 |
70 | const todos = (state: TodosIntialState = initialState, action: FetchingTodos) =>
71 | produce(state, (draft) => {
72 | switch (action.type) {
73 | case FETCHING_TODOS_REQUEST: {
74 | draft.fetchTodosLoading = true
75 | draft.fetchTodosLoading = false
76 | break
77 | }
78 | case FETCHING_TODOS_SUCCESS: {
79 | draft.fetchTodosLoading = false
80 | draft.fetchTodosSuccess = true
81 | draft.todos = draft.todos.concat(action.data)
82 | break
83 | }
84 | case FETCHING_TODOS_FAILURE: {
85 | draft.fetchTodosSuccess = false
86 | draft.fetchTodosFailure = action.error
87 | break
88 | }
89 | default:
90 | return state
91 | }
92 | })
93 |
94 | export default todos
95 |
--------------------------------------------------------------------------------
/src/sagas/comments/index.tsx:
--------------------------------------------------------------------------------
1 | import { put } from '@redux-saga/core/effects'
2 | import axios from 'axios'
3 | import { all, call, fork, takeLatest } from 'redux-saga/effects'
4 | import { FetchCommentsType, WriteCommentType } from 'src/types'
5 | import {
6 | DeleteCommentRequest,
7 | DELETE_COMMENT_FAILURE,
8 | DELETE_COMMENT_REQUEST,
9 | DELETE_COMMENT_SUCCESS,
10 | FetchCommentsRequest,
11 | FETCH_COMMENTS_FAILURE,
12 | FETCH_COMMENTS_REQUEST,
13 | FETCH_COMMENTS_SUCCESS,
14 | UpdateCommentRequest,
15 | UPDATE_COMMENT_FAILURE,
16 | UPDATE_COMMENT_REQUEST,
17 | UPDATE_COMMENT_SUCCESS,
18 | WriteCommentRequest,
19 | WRITE_COMMENT_FAILURE,
20 | WRITE_COMMENT_REQUEST,
21 | WRITE_COMMENT_SUCCESS,
22 | } from 'src/reducers/comments'
23 |
24 | async function fetchCommentsAPI(id: number) {
25 | try {
26 | const response = await axios.get(`/api/v1/posts/${id}/comments`)
27 | return response.data
28 | } catch (err) {
29 | console.error(err)
30 | }
31 | }
32 |
33 | function* fetchComments(action: FetchCommentsRequest) {
34 | try {
35 | const result: FetchCommentsType = yield call(fetchCommentsAPI, action.data)
36 | yield put({
37 | type: FETCH_COMMENTS_SUCCESS,
38 | data: result,
39 | })
40 | } catch (err) {
41 | console.error(err)
42 | yield put({
43 | type: FETCH_COMMENTS_FAILURE,
44 | data: err,
45 | })
46 | }
47 | }
48 |
49 | async function writeCommentAPI(data: WriteCommentType) {
50 | try {
51 | const response = await axios.post(`/api/v1/posts/${data.id}/comments`, data)
52 | return response.status
53 | } catch (err) {
54 | console.error(err)
55 | }
56 | }
57 |
58 | function* writeComment(action: WriteCommentRequest) {
59 | try {
60 | const result: number = yield call(writeCommentAPI, action.data)
61 | if (result === 200) {
62 | yield put({
63 | type: WRITE_COMMENT_SUCCESS,
64 | })
65 | }
66 | } catch (err) {
67 | console.error(err)
68 | yield put({
69 | type: WRITE_COMMENT_FAILURE,
70 | data: err,
71 | })
72 | }
73 | }
74 |
75 | async function deleteCommentAPI(data: { postId: number; commentId: number }) {
76 | try {
77 | const response = await axios.delete(`/api/v1/posts/${data.postId}/comments/${data.commentId}`)
78 | return response.status
79 | } catch (err) {
80 | console.error(err)
81 | }
82 | }
83 |
84 | function* deleteComment(action: DeleteCommentRequest) {
85 | try {
86 | const result: number = yield call(deleteCommentAPI, action.data)
87 | if (result === 200) {
88 | yield put({
89 | type: DELETE_COMMENT_SUCCESS,
90 | })
91 | }
92 | } catch (err) {
93 | console.error(err)
94 | yield put({
95 | type: DELETE_COMMENT_FAILURE,
96 | data: err,
97 | })
98 | }
99 | }
100 |
101 | async function updateCommentAPI(data: { postId: number; commentId: number; text: string }) {
102 | try {
103 | const response = await axios.put(`/api/v1/posts/${data.postId}/comments/${data.commentId}`, data)
104 | return response.status
105 | } catch (err) {
106 | console.error(err)
107 | }
108 | }
109 |
110 | function* updateComment(action: UpdateCommentRequest) {
111 | try {
112 | const result: number = yield call(updateCommentAPI, action.data)
113 | if (result === 200) {
114 | yield put({
115 | type: UPDATE_COMMENT_SUCCESS,
116 | data: action.data,
117 | })
118 | }
119 | } catch (err) {
120 | console.error(err)
121 | yield put({
122 | type: UPDATE_COMMENT_FAILURE,
123 | data: err,
124 | })
125 | }
126 | }
127 |
128 | function* watchFetchComments() {
129 | yield takeLatest(FETCH_COMMENTS_REQUEST, fetchComments)
130 | }
131 |
132 | function* watchWriteComment() {
133 | yield takeLatest(WRITE_COMMENT_REQUEST, writeComment)
134 | }
135 |
136 | function* watchDeleteComment() {
137 | yield takeLatest(DELETE_COMMENT_REQUEST, deleteComment)
138 | }
139 |
140 | function* watchUpdateComment() {
141 | yield takeLatest(UPDATE_COMMENT_REQUEST, updateComment)
142 | }
143 |
144 | export default function* commentSaga() {
145 | yield all([fork(watchFetchComments), fork(watchWriteComment), fork(watchDeleteComment), fork(watchUpdateComment)])
146 | }
147 |
--------------------------------------------------------------------------------
/src/sagas/home/index.tsx:
--------------------------------------------------------------------------------
1 | import { all, call, fork, put, takeLatest } from '@redux-saga/core/effects'
2 | import axios from 'axios'
3 | import {
4 | FETCHING_HOME_POSTS_FAILURE,
5 | FETCHING_HOME_POSTS_REQUEST,
6 | FETCHING_HOME_POSTS_SUCCESS,
7 | } from 'src/reducers/home'
8 | import { homeData } from 'src/types'
9 |
10 | async function fetchHomePostsAPI() {
11 | try {
12 | const response = await axios.get('api/v1/home/posts')
13 | return response.data
14 | } catch (err) {
15 | console.error(err)
16 | }
17 | }
18 |
19 | function* fetchHomePosts() {
20 | try {
21 | const result: homeData = yield call(fetchHomePostsAPI)
22 | yield put({
23 | type: FETCHING_HOME_POSTS_SUCCESS,
24 | data: result,
25 | })
26 | } catch (err) {
27 | console.error(err)
28 | yield put({
29 | type: FETCHING_HOME_POSTS_FAILURE,
30 | data: err,
31 | })
32 | }
33 | }
34 |
35 | function* watchFetchHomePosts() {
36 | yield takeLatest(FETCHING_HOME_POSTS_REQUEST, fetchHomePosts)
37 | }
38 |
39 | export default function* homeSaga() {
40 | yield all([fork(watchFetchHomePosts)])
41 | }
42 |
--------------------------------------------------------------------------------
/src/sagas/index.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { all, fork } from 'redux-saga/effects'
3 | import postSaga from './posts'
4 | import commentSaga from './comments'
5 | import homeSaga from './home'
6 | import NotificationSaga from './notifications'
7 | import MyPageSaga from './mypage'
8 |
9 | // config에 들어갈 프로퍼티를 default 값으로도 설정할 수 있습니다
10 | axios.defaults.baseURL = 'https://api.wellseecoding.com/'
11 |
12 | /* 로컬 */
13 | // axios.defaults.baseURL = 'http://localhost:8080/'
14 |
15 | axios.defaults.withCredentials = true
16 | axios.defaults.headers = {
17 | ...axios.defaults.headers,
18 | }
19 | export default function* rootSaga() {
20 | yield all([fork(postSaga), fork(commentSaga), fork(homeSaga), fork(NotificationSaga), fork(MyPageSaga)])
21 | }
22 |
--------------------------------------------------------------------------------
/src/sagas/mypage/index.tsx:
--------------------------------------------------------------------------------
1 | import { all, call, fork, put, takeLatest } from '@redux-saga/core/effects'
2 | import axios from 'axios'
3 | import {
4 | FETCHING_MYPAGE_FAILURE,
5 | FETCHING_MYPAGE_REQUEST,
6 | FETCHING_MYPAGE_SUCCESS,
7 | UpdateSelfIntroRequest,
8 | UPDATE_SELF_INTRO_REQUEST,
9 | UPDATE_SELF_INTRO_SUCCESS,
10 | UPDATE_SELF_INTRO_FAILURE,
11 | UpdateSchoolRequest,
12 | UPDATE_SCHOOL_REQUEST,
13 | UPDATE_SCHOOL_SUCCESS,
14 | UPDATE_SCHOOL_FAILURE,
15 | UpdateYearsRequest,
16 | UPDATE_YEARS_REQUEST,
17 | UPDATE_YEARS_SUCCESS,
18 | UPDATE_YEARS_FAILURE,
19 | UpdatePortfolioRequest,
20 | UPDATE_PORTFOLIO_REQUEST,
21 | UPDATE_PORTFOLIO_SUCCESS,
22 | UPDATE_PORTFOLIO_FAILURE,
23 | } from 'src/reducers/mypage'
24 | import { myPage, myPageSelfIntro, myPageEducations, myPageLinks, myPageWorks } from 'src/types'
25 |
26 | async function fetchMyPageAPI() {
27 | try {
28 | const response = await axios.get('api/v1/users/profile')
29 | return response.data
30 | } catch (err) {
31 | console.error(err)
32 | }
33 | }
34 |
35 | function* fetchMyPage() {
36 | try {
37 | const result: myPage = yield call(fetchMyPageAPI)
38 | yield put({
39 | type: FETCHING_MYPAGE_SUCCESS,
40 | data: result,
41 | })
42 | } catch (err) {
43 | console.error(err)
44 | yield put({
45 | type: FETCHING_MYPAGE_FAILURE,
46 | data: err,
47 | })
48 | }
49 | }
50 |
51 | // 자기소개 업데이트
52 | async function updateSelfIntroAPI(data: myPageSelfIntro) {
53 | try {
54 | const response = await axios.put(`/api/v1/users/profile/preface`, data)
55 | return response.status
56 | } catch (err) {
57 | console.error(err)
58 | }
59 | }
60 |
61 | function* updateSelfIntro(action: UpdateSelfIntroRequest) {
62 | try {
63 | yield call(updateSelfIntroAPI, action.data)
64 | yield put({
65 | type: UPDATE_SELF_INTRO_SUCCESS,
66 | })
67 | } catch (err) {
68 | console.error(err)
69 | yield put({
70 | type: UPDATE_SELF_INTRO_FAILURE,
71 | data: err,
72 | })
73 | }
74 | }
75 |
76 | // 학교정보 업데이트
77 | async function updateSchoolAPI(data: myPageEducations) {
78 | try {
79 | const response = await axios.put(`/api/v1/users/profile/education`, data)
80 | return response.status
81 | } catch (err) {
82 | console.error(err)
83 | }
84 | }
85 |
86 | function* updateSchool(action: UpdateSchoolRequest) {
87 | try {
88 | yield call(updateSchoolAPI, action.data)
89 | yield put({
90 | type: UPDATE_SCHOOL_SUCCESS,
91 | })
92 | } catch (err) {
93 | console.error(err)
94 | yield put({
95 | type: UPDATE_SCHOOL_FAILURE,
96 | data: err,
97 | })
98 | }
99 | }
100 |
101 | //경력정보 업데이트
102 | async function updateYearsAPI(data: myPageWorks) {
103 | try {
104 | const response = await axios.put(`/api/v1/users/profile/works`, data)
105 | return response.status
106 | } catch (err) {
107 | console.error(err)
108 | }
109 | }
110 |
111 | function* updateYears(action: UpdateYearsRequest) {
112 | try {
113 | yield call(updateYearsAPI, action.data)
114 | yield put({
115 | type: UPDATE_YEARS_SUCCESS,
116 | })
117 | } catch (err) {
118 | console.error(err)
119 | yield put({
120 | type: UPDATE_YEARS_FAILURE,
121 | data: err,
122 | })
123 | }
124 | }
125 |
126 | //포트폴리오 업데이트
127 | async function updatePortfolioAPI(data: myPageLinks) {
128 | try {
129 | const response = await axios.put(`/api/v1/users/profile/links`, data)
130 | return response.status
131 | } catch (err) {
132 | console.error(err)
133 | }
134 | }
135 |
136 | function* updatePortfolio(action: UpdatePortfolioRequest) {
137 | try {
138 | yield call(updatePortfolioAPI, action.data)
139 | yield put({
140 | type: UPDATE_PORTFOLIO_SUCCESS,
141 | })
142 | } catch (err) {
143 | console.error(err)
144 | yield put({
145 | type: UPDATE_PORTFOLIO_FAILURE,
146 | data: err,
147 | })
148 | }
149 | }
150 |
151 | function* watchFetchMyPage() {
152 | yield takeLatest(FETCHING_MYPAGE_REQUEST, fetchMyPage)
153 | }
154 | function* watchUpdateSelfIntro() {
155 | yield takeLatest(UPDATE_SELF_INTRO_REQUEST, updateSelfIntro)
156 | }
157 | function* watchUpdateSchool() {
158 | yield takeLatest(UPDATE_SCHOOL_REQUEST, updateSchool)
159 | }
160 | function* watchUpdateYears() {
161 | yield takeLatest(UPDATE_YEARS_REQUEST, updateYears)
162 | }
163 | function* watchUpdatePortfoilo() {
164 | yield takeLatest(UPDATE_PORTFOLIO_REQUEST, updatePortfolio)
165 | }
166 | export default function* MyPageSaga() {
167 | yield all([
168 | fork(watchFetchMyPage),
169 | fork(watchUpdateSelfIntro),
170 | fork(watchUpdateSchool),
171 | fork(watchUpdateYears),
172 | fork(watchUpdatePortfoilo),
173 | ])
174 | }
175 |
--------------------------------------------------------------------------------
/src/sagas/notifications/index.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | DELETE_ALL_NOTIS_FAILURE,
4 | DELETE_ALL_NOTIS_REQUEST,
5 | DELETE_ALL_NOTIS_SUCCESS,
6 | FETCHING_NOTIS_FAILURE,
7 | FETCHING_NOTIS_REQUEST,
8 | FETCHING_NOTIS_SUCCESS,
9 | READ_ALL_NOTIS_FAILURE,
10 | READ_ALL_NOTIS_REQUEST,
11 | READ_ALL_NOTIS_SUCCESS,
12 | UpdateNotiRequest,
13 | UPDATE_NOTI_FAILURE,
14 | UPDATE_NOTI_REQUEST,
15 | UPDATE_NOTI_SUCCESS,
16 | } from 'src/reducers/notifications'
17 | import { all, call, fork, takeLatest, put } from 'redux-saga/effects'
18 | import { notificationType } from 'src/types'
19 |
20 | async function fetchNotisAPI() {
21 | try {
22 | const response = await axios.get('/api/v1/users/notifications')
23 | return response.data.notifications
24 | } catch (err) {
25 | console.error(err)
26 | }
27 | }
28 |
29 | function* fetchNotis() {
30 | try {
31 | const result: notificationType[] = yield call(fetchNotisAPI)
32 | if (result.length) {
33 | yield put({
34 | type: FETCHING_NOTIS_SUCCESS,
35 | data: result,
36 | })
37 | }
38 | } catch (err) {
39 | console.error(err)
40 | yield put({
41 | type: FETCHING_NOTIS_FAILURE,
42 | data: err,
43 | })
44 | }
45 | }
46 |
47 | async function updateNotiAPI(data: number) {
48 | try {
49 | const response = await axios.put(`/api/v1/users/notifications/${data}/read`, {})
50 | return response.status
51 | } catch (err) {
52 | console.error(err)
53 | }
54 | }
55 |
56 | function* updateNoti(action: UpdateNotiRequest) {
57 | try {
58 | const result: number = yield call(updateNotiAPI, action.data)
59 | if (result === 200) {
60 | yield put({
61 | type: UPDATE_NOTI_SUCCESS,
62 | data: action.data,
63 | })
64 | }
65 | } catch (err) {
66 | console.error(err)
67 | yield put({
68 | type: UPDATE_NOTI_FAILURE,
69 | data: err,
70 | })
71 | }
72 | }
73 |
74 | async function readAllNotisAPI() {
75 | try {
76 | const response = await axios.put('/api/v1/users/notifications/read', {})
77 | return response.status
78 | } catch (err) {
79 | console.error(err)
80 | }
81 | }
82 |
83 | function* readAllNotis() {
84 | try {
85 | const result: number = yield call(readAllNotisAPI)
86 | if (result === 200) {
87 | yield put({
88 | type: READ_ALL_NOTIS_SUCCESS,
89 | })
90 | }
91 | } catch (err) {
92 | console.error(err)
93 | yield put({
94 | type: READ_ALL_NOTIS_FAILURE,
95 | data: err,
96 | })
97 | }
98 | }
99 |
100 | async function deleteAllNotisAPI() {
101 | try {
102 | const response = await axios.delete('/api/v1/users/notifications')
103 | return response.status
104 | } catch (err) {
105 | console.error(err)
106 | }
107 | }
108 |
109 | function* deleteAllNotis() {
110 | try {
111 | const result: number = yield call(deleteAllNotisAPI)
112 | if (result === 200) {
113 | yield put({
114 | type: DELETE_ALL_NOTIS_SUCCESS,
115 | })
116 | }
117 | } catch (err) {
118 | console.error(err)
119 | yield put({
120 | type: DELETE_ALL_NOTIS_FAILURE,
121 | data: err,
122 | })
123 | }
124 | }
125 |
126 | function* watchFetchNotis() {
127 | yield takeLatest(FETCHING_NOTIS_REQUEST, fetchNotis)
128 | }
129 |
130 | function* watchUpdateNoti() {
131 | yield takeLatest(UPDATE_NOTI_REQUEST, updateNoti)
132 | }
133 |
134 | function* watchReadAllNotis() {
135 | yield takeLatest(READ_ALL_NOTIS_REQUEST, readAllNotis)
136 | }
137 |
138 | function* watchDeleteAllNotis() {
139 | yield takeLatest(DELETE_ALL_NOTIS_REQUEST, deleteAllNotis)
140 | }
141 |
142 | export default function* NotificationSaga() {
143 | yield all([fork(watchFetchNotis), fork(watchUpdateNoti), fork(watchReadAllNotis), fork(watchDeleteAllNotis)])
144 | }
145 |
--------------------------------------------------------------------------------
/src/store/index.tsx:
--------------------------------------------------------------------------------
1 | import { createWrapper } from 'next-redux-wrapper'
2 | import { applyMiddleware, createStore } from 'redux'
3 | import { composeWithDevTools } from 'redux-devtools-extension'
4 | import createSagaMiddleware from 'redux-saga'
5 |
6 | import rootReducer from 'src/reducers'
7 | import rootSaga from 'src/sagas'
8 |
9 | const isDev = process.env.NODE_ENV === 'development'
10 |
11 | const configureStore = () => {
12 | const sagaMiddleware = createSagaMiddleware()
13 | const middlewares = [sagaMiddleware]
14 | const enhancer = composeWithDevTools(applyMiddleware(...middlewares))
15 | const store: any = createStore(rootReducer, enhancer)
16 |
17 | store.sagaTask = sagaMiddleware.run(rootSaga)
18 | return store
19 | }
20 |
21 | const wrapper = createWrapper(configureStore, {
22 | debug: isDev,
23 | })
24 |
25 | export default wrapper
26 |
--------------------------------------------------------------------------------
/src/styles/common.tsx:
--------------------------------------------------------------------------------
1 | export const Common = {
2 | // 🎨 숫자가 작을 수록 채도가 진한 색상
3 | colors: {
4 | orange: '',
5 | orange01: '#ff966c',
6 | orange02: '#ffb89b',
7 | orange03: '#ffdacb',
8 | orange04: '#ffeee7',
9 | orange05: '#fff8f5',
10 |
11 | yellow: '#ffe920',
12 | black: '#262626',
13 |
14 | gray: '#444241',
15 | gray01: '#696766',
16 | gray02: '#8f8c8b',
17 | gray03: '#b6b2b0',
18 | gray04: '#d3cfcc',
19 | gray05: '#efebe8',
20 | gray06: '#f5f5f5',
21 | },
22 |
23 | fontSize: {
24 | bigTitle: '4rem',
25 | title: '2.4rem',
26 | fs22: '2.2rem',
27 | fs20: '2rem',
28 | fs18: '1.8rem',
29 | fs16: '1.6rem',
30 | fs14: '1.4rem',
31 | },
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/global-styles.tsx:
--------------------------------------------------------------------------------
1 | import emotionReset from 'emotion-reset'
2 | import { Global, css } from '@emotion/react'
3 | import React from 'react'
4 | import { Common } from './common'
5 |
6 | export const textEllipsis = css`
7 | text-overflow: ellipsis;
8 | white-space: nowrap;
9 | overflow: hidden;
10 | `
11 |
12 | export const hideScrollBar = css`
13 | -ms-overflow-style: none; /* IE and Edge */
14 | scrollbar-width: none; /* Firefox */
15 | &::-webkit-scrollbar {
16 | display: none; /* Chrome, Safari, Opera */
17 | }
18 | `
19 |
20 | export const GlobalStyles = (
21 |
131 | )
132 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export type homeData = {
2 | createdGroups: PostType[]
3 | registeredGroups: PostType[]
4 | appliedGroups: PostType[]
5 | likedGroups: PostType[]
6 | }
7 |
8 | export type TodoType = {
9 | completed: boolean
10 | id: number
11 | title: string
12 | userId: number
13 | }
14 |
15 | export type MemberData = {
16 | userId: number
17 | postId: number
18 | name: string
19 | authorized: boolean
20 | }
21 |
22 | export type PostData = {
23 | theme: string
24 | posts: PostType[]
25 | }
26 |
27 | /* 서버로부터 받아오는 알림 배열에 대한 타입 정의 */
28 |
29 | export type notificationType = {
30 | id: number
31 | senderUserId: number
32 | senderUserName: string
33 | receiverUserId: number
34 | receiverUserName: string
35 | postId: number
36 | postTitle: string
37 | eventCategory: string
38 | timestamp: number
39 | read: boolean
40 | }
41 |
42 | /* 서버로부터 받아오는 Posts 배열의 한 객체당 타입 */
43 | export type PostType = {
44 | id: number
45 | userId: number
46 | userName: string
47 | name: string | ''
48 | deadline: string | ''
49 | schedule: string | ''
50 | summary: string | ''
51 | qualification: string | ''
52 | size: string | ''
53 | tags: []
54 | commentCount: number
55 | }
56 |
57 | /* 내가 쓴 글 보낼 때 필요한 타입 */
58 | export type WritePostType = {
59 | name: string
60 | deadline: string
61 | schedule: string
62 | summary: string
63 | qualification: string
64 | size: string
65 | tags: []
66 | id?: number
67 | }
68 |
69 | export type CommentType = {
70 | id: number
71 | name: string
72 | job: string
73 | text: string
74 | me: boolean
75 | date: string
76 | }
77 |
78 | export type FetchCommentsType = {
79 | userId: number
80 | userName: string
81 | commentId: number
82 | commentDate: number
83 | text: string
84 | deleted: boolean
85 | children: FetchCommentsType[]
86 | }
87 |
88 | export type WriteCommentType = {
89 | id: number
90 | parentId: number
91 | text: string
92 | }
93 |
94 | export type UpdateCommentType = {
95 | postId: number
96 | commentId: number
97 | text: string
98 | }
99 |
100 | //----------------------------------------
101 | // 마이페이지
102 | export type myPage = {
103 | status: string
104 | aboutMe: string
105 | job: string
106 | tags: string[]
107 | educations: myPageEducations[]
108 | works: myPageWorks[]
109 | links: myPageLinks[]
110 | }
111 |
112 | // 학교 업데이트
113 | export type myPageEducations = {
114 | major: string
115 | degree: string
116 | graduated: boolean
117 | }
118 |
119 | // 경력 업데이트
120 | export type myPageWorks = {
121 | role: string
122 | technology: string
123 | years: number
124 | }
125 |
126 | // 포트폴리오 업데이트
127 | export type myPageLinks = {
128 | name: string
129 | link: string
130 | description: string
131 | }
132 |
133 | // 자기소개 업데이트
134 | export type myPageSelfIntro = {
135 | aboutMe: string
136 | tags: string[]
137 | job: string
138 | }
139 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "jsx": "preserve",
6 | "lib": ["dom", "es2017"],
7 | "moduleResolution": "node",
8 | "allowJs": true,
9 | "noEmit": true,
10 | "strict": true,
11 | "allowSyntheticDefaultImports": true,
12 | "skipLibCheck": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "removeComments": false,
16 | "preserveConstEnums": true,
17 | "sourceMap": true,
18 | "esModuleInterop": true,
19 | "forceConsistentCasingInFileNames": true,
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "types": ["@emotion/react/types/css-prop"],
23 | "baseUrl": ".",
24 | "paths":{
25 | "components/*":["components/*"],
26 | "config/*":["config/*"],
27 | "pages/*":["pages/*"],
28 | "styles/*":["styles/*"]
29 | }
30 | },
31 | "exclude": ["node_modules"],
32 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
33 | }
34 |
--------------------------------------------------------------------------------