├── src ├── react-app-env.d.ts ├── assets │ ├── custom.d.ts │ ├── images │ │ ├── img_glass.webp │ │ ├── img_link.webp │ │ ├── img_landing.webp │ │ ├── img_mypage.webp │ │ ├── img_main_page.webp │ │ ├── img_teamsoseo.webp │ │ ├── img_no_teammates.png │ │ ├── img_background_ellipse.svg │ │ ├── img_page_1.svg │ │ ├── img_page_2.svg │ │ ├── img_empty_profile.svg │ │ ├── img_team_default.svg │ │ ├── img_empty_profile_small.svg │ │ ├── img_empty_join.svg │ │ ├── img_logo.svg │ │ ├── img_logo_header.svg │ │ ├── img_empty_feedback.svg │ │ └── img_computer.svg │ ├── icons │ │ ├── ic_arrow_down.svg │ │ ├── ic_back.svg │ │ ├── ic_message.svg │ │ ├── ic_whole.svg │ │ ├── ic_arrow_up.svg │ │ ├── ic_arrow_view_more.svg │ │ ├── ic_pick.svg │ │ ├── ic_arrow_view_all.svg │ │ ├── ic_arrow_right_gray_5.svg │ │ ├── ic_arrow_right.svg │ │ ├── ic_arrow_view_more_close.svg │ │ ├── ic_leave_team.svg │ │ ├── ic_arrow_detail.svg │ │ ├── ic_coral_check.svg │ │ ├── ic_gray_check.svg │ │ ├── ic_member_added.svg │ │ ├── ic_search.svg │ │ ├── ic_meatball.svg │ │ ├── ic_plus.svg │ │ ├── ic_member_add.svg │ │ ├── ic_plus_coral.svg │ │ ├── ic_warning.svg │ │ ├── ic_edit.svg │ │ ├── ic_edit_profile.svg │ │ ├── ic_input_pencil.svg │ │ ├── ic_pencil.svg │ │ ├── ic_mypage_edit.svg │ │ ├── ic_bookmark_selected.svg │ │ ├── ic_bookmark_unselected.svg │ │ ├── ic_profile.svg │ │ ├── ic_kakao.svg │ │ ├── ic_new_tag.svg │ │ ├── ic_camera.svg │ │ ├── ic_unpicked.svg │ │ ├── ic_picked.svg │ │ ├── ic_lock.svg │ │ ├── ic_trash.svg │ │ ├── ic_camera_main_coral.svg │ │ ├── ic_empty_keyword.svg │ │ ├── ic_email.svg │ │ ├── ic_empty_feedback.svg │ │ ├── ic_link_copy.svg │ │ ├── ic_setting.svg │ │ ├── ic_crown.svg │ │ ├── ic_link_white.svg │ │ ├── ic_link_coral.svg │ │ ├── ic_copy_mypage.svg │ │ ├── ic_link.svg │ │ └── ic_paper_airplane.svg │ └── lottie │ │ ├── TeamLottie │ │ └── index.tsx │ │ └── NeogaLottie │ │ └── index.tsx ├── infrastructure │ ├── api │ │ ├── header.ts │ │ ├── neososeo-form.ts │ │ ├── login-user.ts │ │ ├── service-report.ts │ │ ├── types │ │ │ └── neososeo-form.ts │ │ ├── neoga.ts │ │ └── user.ts │ ├── mock │ │ ├── header.ts │ │ ├── login-user.data.ts │ │ ├── service-report.ts │ │ ├── neososeo-form.ts │ │ ├── neososeo-form.data.ts │ │ └── login-user.ts │ └── remote │ │ ├── header.ts │ │ └── service-report.ts ├── presentation │ ├── pages │ │ ├── Home │ │ │ ├── style.ts │ │ │ ├── index.tsx │ │ │ ├── Team │ │ │ │ ├── style.ts │ │ │ │ └── Invitation │ │ │ │ │ └── style.ts │ │ │ └── MyPage │ │ │ │ ├── Keyword │ │ │ │ └── style.ts │ │ │ │ ├── TeamPick │ │ │ │ └── style.ts │ │ │ │ └── NeogaPick │ │ │ │ └── style.ts │ │ ├── JoinComplete │ │ │ └── index.tsx │ │ ├── Team │ │ │ ├── Alert │ │ │ │ └── style.ts │ │ │ ├── Main │ │ │ │ └── MemberPopup │ │ │ │ │ └── index.tsx │ │ │ ├── Edit │ │ │ │ └── style.ts │ │ │ ├── Register │ │ │ │ └── style.ts │ │ │ ├── Issue │ │ │ │ └── Keyword │ │ │ │ │ └── style.ts │ │ │ └── Member │ │ │ │ └── index.tsx │ │ ├── Login │ │ │ ├── OAuth.ts │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NeososeoForm │ │ │ ├── Answer │ │ │ │ └── style.ts │ │ │ ├── index.tsx │ │ │ ├── Finish │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── Home │ │ │ │ └── style.ts │ │ ├── Neoga │ │ │ ├── Result │ │ │ │ └── style.ts │ │ │ └── Create │ │ │ │ └── style.ts │ │ ├── Join │ │ │ └── style.ts │ │ └── OAuthRedirectHandler │ │ │ └── index.tsx │ ├── components │ │ ├── common │ │ │ ├── Loader │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ ├── Toast │ │ │ │ ├── List │ │ │ │ │ ├── style.ts │ │ │ │ │ └── index.tsx │ │ │ │ └── Item │ │ │ │ │ ├── style.ts │ │ │ │ │ └── index.tsx │ │ │ ├── Empty │ │ │ │ ├── UserSearch │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── Keyword │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── Feedback │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── HomeTeam │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── HomeNeoga │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── MyPick │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── MyPage │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ └── NeogaDetailForm │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ ├── ScrollToTop │ │ │ │ └── index.tsx │ │ │ ├── ExpandListButton │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ ├── Skeleton │ │ │ │ ├── style.ts │ │ │ │ ├── TemplateList │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── TeamProfile │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── NSSPick │ │ │ │ │ └── index.tsx │ │ │ │ ├── MyPageInfo │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamIssue │ │ │ │ │ └── index.tsx │ │ │ │ ├── TSSPick │ │ │ │ │ └── index.tsx │ │ │ │ └── CardItem │ │ │ │ │ └── index.tsx │ │ │ ├── ProfileList │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ ├── ProfileAddButton │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── IssueTeamInfo │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── ModalWrapper │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ ├── FormItem │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── Label │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── ProfileItem │ │ │ │ └── index.tsx │ │ │ ├── Modal │ │ │ │ ├── DeleteIssue │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── HostDelegation │ │ │ │ │ └── style.ts │ │ │ │ ├── TeamLeave │ │ │ │ │ └── style.ts │ │ │ │ ├── style.ts │ │ │ │ └── DeleteFeedback │ │ │ │ │ └── index.tsx │ │ │ ├── IssueMemberList │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── Navigation │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── Keyword │ │ │ │ ├── style.ts │ │ │ │ ├── ImmutableList │ │ │ │ │ └── index.tsx │ │ │ │ ├── Item │ │ │ │ │ ├── style.ts │ │ │ │ │ └── index.tsx │ │ │ │ └── MutableList │ │ │ │ │ └── index.tsx │ │ │ ├── HomeHeader │ │ │ │ └── style.ts │ │ │ ├── IssueCardList │ │ │ │ └── index.tsx │ │ │ ├── ImageUpload │ │ │ │ └── style.ts │ │ │ ├── SelectBox │ │ │ │ └── style.ts │ │ │ ├── Input │ │ │ │ └── style.ts │ │ │ ├── BottomSheet │ │ │ │ └── MyPageEdit │ │ │ │ │ └── index.tsx │ │ │ ├── IssueCard │ │ │ │ └── style.ts │ │ │ └── Header │ │ │ │ └── style.ts │ │ ├── FeedbackCard │ │ │ ├── List │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ └── ExpandableList │ │ │ │ └── index.tsx │ │ ├── NeogaCreateCard │ │ │ ├── List │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ └── Item │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ ├── GoogleAdsense │ │ │ ├── style.ts │ │ │ └── index.tsx │ │ ├── UserSearchResult │ │ │ ├── style.ts │ │ │ └── index.tsx │ │ ├── NeososeoFormHeader │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NeogaMainCard │ │ │ ├── List │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ └── Item │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ ├── NeososeoAnswerCard │ │ │ ├── List │ │ │ │ └── index.tsx │ │ │ ├── Item │ │ │ │ └── style.ts │ │ │ └── ExpandableList │ │ │ │ └── index.tsx │ │ ├── TeamMemberAdd │ │ │ ├── ForEdit │ │ │ │ └── style.ts │ │ │ └── ForRegister │ │ │ │ └── style.ts │ │ ├── MyItem │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NeogaFormTicket │ │ │ └── index.tsx │ │ ├── JoinCompleteForm │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NeogaResultComment │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NeogaFormImageToSave │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── ProfileListSelectable │ │ │ └── index.tsx │ │ ├── NeogaDetailFormCard │ │ │ └── index.tsx │ │ ├── NeogaResultCard │ │ │ └── index.tsx │ │ └── InAppBrowserEscape │ │ │ └── index.tsx │ ├── routes │ │ ├── common │ │ │ ├── PublicRoute.tsx │ │ │ └── PrivateRoute.tsx │ │ ├── PreferencesRouter.tsx │ │ ├── NeogaRouter.tsx │ │ ├── HomeRouter.tsx │ │ └── FormRouter.tsx │ └── style │ │ ├── common │ │ ├── input.ts │ │ ├── button.ts │ │ ├── color.ts │ │ ├── modal.ts │ │ └── animation.ts │ │ └── global │ │ └── index.ts ├── application │ ├── utils │ │ ├── array.ts │ │ ├── etc.ts │ │ ├── copyClipboard.ts │ │ ├── string.ts │ │ ├── constant.ts │ │ └── browser.ts │ ├── stores │ │ ├── toast.ts │ │ ├── team.ts │ │ ├── neososeo-form.ts │ │ └── login-user.ts │ └── hooks │ │ ├── queries │ │ ├── user.ts │ │ ├── neososeo-form.ts │ │ └── team.ts │ │ ├── useToast.ts │ │ ├── useGoogleAnalytics.ts │ │ ├── useScrollHeight.ts │ │ └── useImageUpload.ts ├── setupTests.ts ├── index.tsx └── App.tsx ├── .vscode └── settings.json ├── public └── favicon.ico ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── comment-pr.yml │ └── lint.yml ├── craco.config.js ├── .prettierrc.json ├── .gitignore ├── tsconfig.json ├── tsconfig.paths.json └── .eslintrc.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode" 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const src: string; 3 | export default src; 4 | } 5 | -------------------------------------------------------------------------------- /src/infrastructure/api/header.ts: -------------------------------------------------------------------------------- 1 | export interface HeaderService { 2 | getIsNotice(): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/images/img_glass.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/src/assets/images/img_glass.webp -------------------------------------------------------------------------------- /src/assets/images/img_link.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/src/assets/images/img_link.webp -------------------------------------------------------------------------------- /src/assets/images/img_landing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/src/assets/images/img_landing.webp -------------------------------------------------------------------------------- /src/assets/images/img_mypage.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/src/assets/images/img_mypage.webp -------------------------------------------------------------------------------- /src/assets/images/img_main_page.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/src/assets/images/img_main_page.webp -------------------------------------------------------------------------------- /src/assets/images/img_teamsoseo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/src/assets/images/img_teamsoseo.webp -------------------------------------------------------------------------------- /src/assets/images/img_no_teammates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neogasogaeseo/Naega-Web/HEAD/src/assets/images/img_no_teammates.png -------------------------------------------------------------------------------- /src/presentation/pages/Home/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StHome = styled.div` 4 | height: 100vh; 5 | display: flex; 6 | flex-direction: column; 7 | `; 8 | -------------------------------------------------------------------------------- /src/assets/images/img_background_ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/presentation/components/common/Loader/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StCommonLoader = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | `; 7 | -------------------------------------------------------------------------------- /src/presentation/components/FeedbackCard/List/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StFeedbackCardList = styled.div` 4 | & > div:last-child { 5 | border-bottom: none; 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/presentation/pages/JoinComplete/index.tsx: -------------------------------------------------------------------------------- 1 | import JoinCompleteForm from '@components/JoinCompleteForm'; 2 | 3 | function joinComplete() { 4 | return ; 5 | } 6 | 7 | export default joinComplete; 8 | -------------------------------------------------------------------------------- /src/application/utils/array.ts: -------------------------------------------------------------------------------- 1 | function randomSelect(arr: T[]): T { 2 | const length = arr.length; 3 | const randomIndex = Math.floor(Math.random() * length); 4 | return arr[randomIndex]; 5 | } 6 | 7 | export { randomSelect }; 8 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaCreateCard/List/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StNeogaCreateCardList = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | gap: 8px; 8 | `; 9 | -------------------------------------------------------------------------------- /src/application/utils/etc.ts: -------------------------------------------------------------------------------- 1 | export const getRandomID = () => String(new Date().getTime()); 2 | 3 | export const getGeneralLocationParams = () => { 4 | const currentLocation = window.location; 5 | return currentLocation.pathname.split('/'); 6 | }; 7 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_message.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/ic_whole.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_view_more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_pick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/img_page_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/img_page_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_view_all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_right_gray_5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## ✨ 어떤 기능인가요? 11 | 12 | ## ✅ To Dos 13 | 14 | - [ ] 15 | - [ ] 16 | - [ ] 17 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_view_more_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_leave_team.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/assets/icons/ic_arrow_detail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/presentation/pages/Team/Alert/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StTeamNoticeItemContainer = styled.div` 4 | padding-top: 46px; 5 | padding-bottom: 46px; 6 | display: flex; 7 | flex-direction: column; 8 | gap: 28px; 9 | `; 10 | -------------------------------------------------------------------------------- /src/assets/icons/ic_coral_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/ic_gray_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/ic_member_added.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoAlias = require('craco-alias'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoAlias, 7 | options: { 8 | source: 'tsconfig', 9 | tsConfigPath: 'tsconfig.paths.json', 10 | }, 11 | }, 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "endOfLine": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/ic_search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/application/stores/toast.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | export interface Toast { 4 | id?: string; 5 | content: string; 6 | duration?: number; 7 | bottom?: number; 8 | } 9 | 10 | export const toastState = atom({ 11 | key: 'toastState', 12 | default: [], 13 | }); 14 | -------------------------------------------------------------------------------- /src/infrastructure/api/neososeo-form.ts: -------------------------------------------------------------------------------- 1 | import { NeososeoAnswerData, NeososeoFormData } from './types/neososeo-form'; 2 | 3 | export interface NeososeoFormService { 4 | getFormInfo(q: string): Promise; 5 | postFormAnswer(body: NeososeoAnswerData): Promise<{ isSuccess: boolean }>; 6 | } 7 | -------------------------------------------------------------------------------- /src/application/stores/team.ts: -------------------------------------------------------------------------------- 1 | import { SearchedUserForEdit, SearchedUserForRegister } from '@api/types/team'; 2 | import { atom } from 'recoil'; 3 | 4 | export const selectedUserListState = atom>({ 5 | key: 'selectedUserListState', 6 | default: [], 7 | }); 8 | -------------------------------------------------------------------------------- /src/infrastructure/api/login-user.ts: -------------------------------------------------------------------------------- 1 | import { LoginUser, User } from './types/user'; 2 | 3 | export interface LoginUserService { 4 | getUserInfo(token: string): Promise; 5 | postLogin(authorizationCode: string): Promise; 6 | postUserInfo(userInfo: FormData): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/icons/ic_meatball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/presentation/components/common/Toast/List/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StToastList = styled.div` 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | position: fixed; 8 | z-index: 1000; 9 | width: calc(100% - 40px); 10 | max-width: 350px; 11 | margin: 0 auto; 12 | `; 13 | -------------------------------------------------------------------------------- /src/assets/icons/ic_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import { RecoilRoot } from 'recoil'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | 10 | 11 | , 12 | document.getElementById('root'), 13 | ); 14 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/UserSearch/index.tsx: -------------------------------------------------------------------------------- 1 | import { StUserSearchEmptyView } from './style'; 2 | 3 | export default function UserSearchEmptyView() { 4 | return ( 5 | 6 | 검색결과 없음 7 | 팀원 아이디를 검색해 팀으로 초대하세요 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/infrastructure/api/service-report.ts: -------------------------------------------------------------------------------- 1 | export interface ReportService { 2 | getServiceCenterCategories(): Promise<{ id: number; content: string }[]>; 3 | postReport( 4 | categoryID: number, 5 | title: string, 6 | content: string, 7 | email: string, 8 | image?: File, 9 | ): Promise<{ isSuccess: boolean }>; 10 | } 11 | -------------------------------------------------------------------------------- /src/presentation/components/GoogleAdsense/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import styled from 'styled-components'; 3 | 4 | export const StDummyAdvertiseBlock = styled.div` 5 | height: 50px; 6 | background-color: ${COLOR.WHITE}; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | `; 12 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/Keyword/index.tsx: -------------------------------------------------------------------------------- 1 | import { StKeywordEmptyView } from './style'; 2 | 3 | function KeywordEmptyView() { 4 | return ( 5 | 6 | 아직 키워드가 없어요 7 | 키워드를 생성해 보세요 8 | 9 | ); 10 | } 11 | 12 | export default KeywordEmptyView; 13 | -------------------------------------------------------------------------------- /src/presentation/components/common/ScrollToTop/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | export default function ScrollToTop() { 5 | const { pathname } = useLocation(); 6 | 7 | useEffect(() => { 8 | window.scrollTo(0, 0); 9 | }, [pathname]); 10 | 11 | return null; 12 | } 13 | -------------------------------------------------------------------------------- /src/presentation/pages/Login/OAuth.ts: -------------------------------------------------------------------------------- 1 | import { DOMAIN } from '@utils/constant'; 2 | 3 | const CLIENT_ID = `${process.env.REACT_APP_CLIENT_ID}`; 4 | const REDIRECT_URI = `${DOMAIN}/auth/kakao/callback`; 5 | 6 | export const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`; 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## ⛓ Related Issues 2 | - close #issue_number 3 | 4 | ## 📋 작업 내용 5 | - [x] ~ 기능 구현 6 | - [x] ~ 페이지 구조화 및 스타일링 7 | 8 | ## 📌 PR Point 9 | - 무슨 이유로 어떻게 코드를 변경했는지 10 | - 어떤 위험이나 우려가 발견되었는지 11 | - 어떤 부분에 리뷰어가 집중해야 하는지 12 | 13 | ## 👀 스크린샷 / GIF / 링크 14 | 15 | ## 🔬 Reference 16 | - 구현에 참고한 링크 (필요한 경우만 작성하고 없으면 지우기) 17 | -------------------------------------------------------------------------------- /src/assets/icons/ic_member_add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/ic_plus_coral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/Feedback/index.tsx: -------------------------------------------------------------------------------- 1 | import { StFeedbackEmptyView } from './style'; 2 | 3 | function FeedbackEmptyView() { 4 | return ( 5 | 6 | 이슈에 대한 피드백이 없어요 7 | 첫번째 피드백을 남겨주세요 8 | 9 | ); 10 | } 11 | 12 | export default FeedbackEmptyView; 13 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/HomeTeam/index.tsx: -------------------------------------------------------------------------------- 1 | import { StHomeTeamEmptyView } from './style'; 2 | 3 | function HomeTeamEmptyView() { 4 | return ( 5 | 6 | 아직 팀원소개서 컨텐츠가 없어요 7 | 팀이나 이슈를 추가해보세요 8 | 9 | ); 10 | } 11 | 12 | export default HomeTeamEmptyView; 13 | -------------------------------------------------------------------------------- /src/presentation/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import HomeHeader from '@components/common/HomeHeader'; 2 | import HomeRouter from '@routes/HomeRouter'; 3 | import { StHome } from './style'; 4 | 5 | function Home() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default Home; 15 | -------------------------------------------------------------------------------- /src/assets/icons/ic_warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/presentation/pages/NeososeoForm/Answer/style.ts: -------------------------------------------------------------------------------- 1 | import { COMMON_INPUT } from '@styles/common/input'; 2 | import styled from 'styled-components'; 3 | 4 | export const StTextarea = styled.textarea` 5 | ${COMMON_INPUT} 6 | width: 100%; 7 | resize: unset; 8 | height: 104px; 9 | `; 10 | 11 | export const StKeywordListWrapper = styled.div` 12 | margin-top: 18px; 13 | `; 14 | -------------------------------------------------------------------------------- /src/presentation/components/common/ExpandListButton/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import styled from 'styled-components'; 3 | 4 | export const StExpandListButton = styled.div` 5 | display: flex; 6 | gap: 6px; 7 | justify-content: center; 8 | align-items: center; 9 | color: ${COLOR.GRAY_4}; 10 | margin: 18px 0; 11 | cursor: pointer; 12 | `; 13 | -------------------------------------------------------------------------------- /src/presentation/routes/common/PublicRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import ErrorGuard from './ErrorGuard'; 3 | 4 | function PublicRoute() { 5 | return ; 6 | } 7 | 8 | function GuardedPublicRoute() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default GuardedPublicRoute; 17 | -------------------------------------------------------------------------------- /src/application/hooks/queries/user.ts: -------------------------------------------------------------------------------- 1 | import { api } from '@api/index'; 2 | 3 | import { useMutation, UseMutationOptions } from 'react-query'; 4 | 5 | export const usePickNeososeoAnswer = (answerID: number, options?: UseMutationOptions) => 6 | useMutation(async () => { 7 | const response = await api.neogaService.postAnswerBookmark(answerID); 8 | return response; 9 | }, options); 10 | -------------------------------------------------------------------------------- /src/assets/icons/ic_edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/infrastructure/mock/header.ts: -------------------------------------------------------------------------------- 1 | import { HeaderService } from '@api/header'; 2 | 3 | export function headerMock(): HeaderService { 4 | const getIsNotice = async () => { 5 | await wait(2000); 6 | return true; 7 | }; 8 | 9 | return { 10 | getIsNotice, 11 | }; 12 | } 13 | 14 | const wait = (milliSeconds: number) => new Promise((resolve) => setTimeout(resolve, milliSeconds)); 15 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StSkeletonItem = styled.div` 4 | @keyframes skeleton-gradient { 5 | 0% { 6 | opacity: 0.5; 7 | } 8 | 100% { 9 | opacity: 1; 10 | } 11 | } 12 | animation: skeleton-gradient 2s infinite ease-in-out; 13 | `; 14 | 15 | export default StSkeletonItem; 16 | -------------------------------------------------------------------------------- /src/application/stores/neososeo-form.ts: -------------------------------------------------------------------------------- 1 | import { NeososeoAnswerData } from '@api/types/neososeo-form'; 2 | import { atom } from 'recoil'; 3 | 4 | export const neososeoAnswerState = atom({ 5 | key: 'neososeoAnswerState', 6 | default: { 7 | userID: 0, 8 | formID: 0, 9 | name: '', 10 | relationID: 0, 11 | answer: '', 12 | keyword: [], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/presentation/components/common/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import BeatLoader from 'react-spinners/BeatLoader'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { StCommonLoader } from './style'; 4 | 5 | function CommonLoader() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default CommonLoader; 14 | -------------------------------------------------------------------------------- /src/assets/icons/ic_edit_profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_input_pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/application/stores/login-user.ts: -------------------------------------------------------------------------------- 1 | import { LoginUser } from '@api/types/user'; 2 | import { INITIAL_LOGIN_USER } from '@utils/constant'; 3 | import { atom } from 'recoil'; 4 | 5 | export const loginUserState = atom({ 6 | key: 'loginUserState', 7 | default: INITIAL_LOGIN_USER, 8 | }); 9 | 10 | export const isAuthenticatedState = atom({ 11 | key: 'isAuthenticatedState', 12 | default: false, 13 | }); 14 | -------------------------------------------------------------------------------- /src/application/utils/copyClipboard.ts: -------------------------------------------------------------------------------- 1 | export const copyClipboard = async ( 2 | text: string, 3 | successAction?: () => void, 4 | failAction?: () => void, 5 | ) => { 6 | try { 7 | if (!text) { 8 | failAction && failAction(); 9 | return; 10 | } 11 | await navigator.clipboard.writeText(text); 12 | successAction && successAction(); 13 | } catch (error) { 14 | failAction && failAction(); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .env 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /src/assets/images/img_empty_profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/ic_pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/ic_mypage_edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/infrastructure/mock/login-user.data.ts: -------------------------------------------------------------------------------- 1 | export const LOGIN_USER_DATA = { 2 | _: { 3 | id: 2, 4 | accessToken: 'token', 5 | refreshToken: '', 6 | username: '서진서진', 7 | userID: 'seojin', 8 | profileImage: 9 | 'https://w.namu.la/s/89faf888a2565101317751cdee5aef473c7af94d571ca10cee82b35ba89bcb5b2849b598ecab6a67095d318bef107a56f1c9c0d2c2fabb20203bf628b55653e0c602c3b04af357c53cb54f581a5d0fe5e642ce66df7c0cf26b5fb4cefbddc844', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/assets/icons/ic_bookmark_selected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_bookmark_unselected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/presentation/components/UserSearchResult/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StUserSearchResultForTeamRegister = styled.div` 7 | width: 100%; 8 | height: 100%; 9 | & > *:first-child { 10 | padding: 40px 20px 12px 20px; 11 | margin-bottom: 7px; 12 | ${FONT_STYLES.SB_13_TITLE} 13 | background-color: ${COLOR.GRAY_1}; 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /src/infrastructure/remote/header.ts: -------------------------------------------------------------------------------- 1 | import { HeaderService } from '@api/header'; 2 | import { STATUS_CODE } from '@utils/constant'; 3 | import { privateAPI } from './base'; 4 | 5 | export function headerRemote(): HeaderService { 6 | const getIsNotice = async () => { 7 | const response = await privateAPI.get({ url: '/user/notice/bar' }); 8 | if (response.status === STATUS_CODE.OK) { 9 | return response.data.notice; 10 | } else throw '서버 통신 실패'; 11 | }; 12 | return { getIsNotice }; 13 | } 14 | -------------------------------------------------------------------------------- /src/presentation/components/common/Toast/List/index.tsx: -------------------------------------------------------------------------------- 1 | import { toastState } from '@stores/toast'; 2 | import { useRecoilValue } from 'recoil'; 3 | import ToastItem from '../Item'; 4 | import { StToastList } from './style'; 5 | 6 | function ToastList() { 7 | const toasts = useRecoilValue(toastState); 8 | return ( 9 | 10 | {toasts.map((toast) => ( 11 | 12 | ))} 13 | 14 | ); 15 | } 16 | 17 | export default ToastList; 18 | -------------------------------------------------------------------------------- /src/assets/images/img_team_default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/HomeNeoga/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import { StHomeNeogaEmptyView } from './style'; 4 | 5 | function HomeNeogaEmptyView() { 6 | const navigate = useNavigate(); 7 | 8 | return ( 9 | 10 | 아직 생성한 너가소개서가 없어요 11 | 첫 너가소개서, 만들어 볼까요? 12 | navigate('/neoga/create')}>너가소개서 만들기 13 | 14 | ); 15 | } 16 | 17 | export default HomeNeogaEmptyView; 18 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/MyPick/index.tsx: -------------------------------------------------------------------------------- 1 | import { StMyPickEmptyView } from './style'; 2 | 3 | interface MyPickEmptyViewProps { 4 | pickType: 'neoga' | 'team'; 5 | } 6 | 7 | function MyPickEmptyView(props: MyPickEmptyViewProps) { 8 | const { pickType } = props; 9 | const text = pickType === 'neoga' ? '답변' : '피드백'; 10 | 11 | return ( 12 | 13 | 아직 픽할 {text}이 없어요 14 | {text}을 받아보세요! 15 | 16 | ); 17 | } 18 | 19 | export default MyPickEmptyView; 20 | -------------------------------------------------------------------------------- /src/presentation/components/NeososeoFormHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { StNeososeoFormHeader } from './style'; 2 | 3 | interface NeososeoFormHeaderProps { 4 | title: string; 5 | image: string; 6 | } 7 | 8 | function NeososeoFormHeader(props: NeososeoFormHeaderProps) { 9 | const { title, image } = props; 10 | return ( 11 | 12 | {title} 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default NeososeoFormHeader; 21 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/TemplateList/index.tsx: -------------------------------------------------------------------------------- 1 | import { StTemplateListSkeleton, StLongText, StShortText, StCardContainer, StCard } from './style'; 2 | 3 | function TemplateListSkeleton() { 4 | return ( 5 | 6 | 7 | 8 | 9 | {new Array(3).fill('').map((_, i) => ( 10 | 11 | ))} 12 | 13 | 14 | ); 15 | } 16 | 17 | export default TemplateListSkeleton; 18 | -------------------------------------------------------------------------------- /src/presentation/style/common/input.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from './font-style'; 4 | 5 | export const COMMON_INPUT = css` 6 | padding: 18px 16px; 7 | border: 1px solid ${COLOR.GRAY_3}; 8 | border-radius: 16px; 9 | ${FONT_STYLES.R_16_TITLE} 10 | color: ${COLOR.GRAY_7}; 11 | &::placeholder { 12 | color: ${COLOR.GRAY_4}; 13 | } 14 | `; 15 | 16 | export const COMMON_LABEL = css` 17 | ${FONT_STYLES.SB_16_TITLE} 18 | color: ${COLOR.GRAY_7}; 19 | `; 20 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/MyPick/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StMyPickEmptyView = styled.div` 7 | text-align: center; 8 | 9 | div:first-child { 10 | margin-top: 70px; 11 | margin-bottom: 12px; 12 | color: ${COLOR.GRAY_4}; 13 | ${FONT_STYLES.SB_18_TITLE}; 14 | } 15 | 16 | div:last-child { 17 | color: ${COLOR.GRAY_35}; 18 | ${FONT_STYLES.M_14_TITLE}; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/assets/icons/ic_profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaMainCard/List/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StNeogaMainCardList = styled.div` 4 | padding: 0 20px; 5 | width: calc(100% + 40px); 6 | margin-left: -20px; 7 | margin-bottom: 44px; 8 | overflow-x: auto; 9 | -ms-overflow-style: none; 10 | scrollbar-width: none; 11 | &::-webkit-scrollbar { 12 | display: none; 13 | } 14 | `; 15 | 16 | export const StCardWrapper = styled.div` 17 | display: flex; 18 | flex-wrap: nowrap; 19 | width: max-content; 20 | min-width: 100%; 21 | `; 22 | -------------------------------------------------------------------------------- /src/presentation/components/common/ProfileList/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StProfileList = styled.div` 4 | overflow-x: auto; 5 | -ms-overflow-style: none; 6 | scrollbar-width: none; 7 | &::-webkit-scrollbar { 8 | display: none; 9 | } 10 | `; 11 | 12 | export const StItemWrapper = styled.div<{ isSquare: boolean }>` 13 | display: flex; 14 | flex-wrap: nowrap; 15 | width: max-content; 16 | min-width: 100%; 17 | 18 | & > div { 19 | margin-right: ${(props) => (props.isSquare ? '14px' : '16px')}; 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/TeamProfile/index.tsx: -------------------------------------------------------------------------------- 1 | import { StTeamImage, StTeamName, StTeamProfileContainer, StTeamProfileTitle } from './style'; 2 | 3 | function TeamProfileSkeleton() { 4 | return ( 5 | <> 6 | 7 | 8 | {new Array(2).fill('').map((_, i) => ( 9 | 10 | 11 | 12 | 13 | ))} 14 | 15 | > 16 | ); 17 | } 18 | 19 | export default TeamProfileSkeleton; 20 | -------------------------------------------------------------------------------- /src/assets/icons/ic_kakao.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/Keyword/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StKeywordEmptyView = styled.div` 6 | padding-top: 44px; 7 | display: flex; 8 | gap: 12px; 9 | flex-direction: column; 10 | align-items: center; 11 | & div:nth-child(1) { 12 | ${FONT_STYLES.SB_18_TITLE} 13 | color: ${COLOR.GRAY_4}; 14 | } 15 | & div:nth-child(2) { 16 | ${FONT_STYLES.M_14_TITLE} 17 | color: ${COLOR.GRAY_35}; 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/infrastructure/mock/service-report.ts: -------------------------------------------------------------------------------- 1 | import { ReportService } from '@api/service-report'; 2 | 3 | export function reportDataMock(): ReportService { 4 | const getServiceCenterCategories = async () => { 5 | return [ 6 | { id: 1, content: '계정' }, 7 | { id: 2, content: '답변 신고' }, 8 | { id: 3, content: '오류 신고 및 피드백' }, 9 | { id: 4, content: '너소서 질문 제안' }, 10 | { id: 5, content: '기타' }, 11 | ]; 12 | }; 13 | 14 | const postReport = async () => { 15 | return { isSuccess: true }; 16 | }; 17 | 18 | return { getServiceCenterCategories, postReport }; 19 | } 20 | -------------------------------------------------------------------------------- /src/presentation/components/common/ProfileAddButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { StAddButton } from './style'; 2 | import { icPlus } from '@assets/icons/index'; 3 | 4 | interface ProfileAddButtonProps { 5 | isSquare: boolean; 6 | onAddClick: () => void; 7 | } 8 | 9 | function ProfileAddButton(props: ProfileAddButtonProps) { 10 | const { isSquare, onAddClick } = props; 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 | > 18 | ); 19 | } 20 | 21 | export default ProfileAddButton; 22 | -------------------------------------------------------------------------------- /src/presentation/components/GoogleAdsense/index.tsx: -------------------------------------------------------------------------------- 1 | import { Adsense } from '@ctrl/react-adsense'; 2 | import { isProduction } from '@utils/constant'; 3 | import { StDummyAdvertiseBlock } from './style'; 4 | 5 | function GoogleAdsense() { 6 | if (!isProduction) { 7 | return ; 8 | } 9 | 10 | return ( 11 | 17 | ); 18 | } 19 | 20 | export default GoogleAdsense; 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | 16 | 17 | ## To Reproduce 18 | 19 | 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 28 | ## Expected behavior 29 | 30 | 33 | 34 | ## Screenshots 35 | 36 | 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/presentation/routes/common/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | 3 | import ErrorGuard from './ErrorGuard'; 4 | import { useLoginUser } from '@hooks/useLoginUser'; 5 | 6 | function PrivateRoute() { 7 | const { isAuthenticated, isJoined } = useLoginUser(); 8 | 9 | if (isAuthenticated) return isJoined ? : ; 10 | 11 | return ; 12 | } 13 | 14 | function GuardedPrivateRoute() { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default GuardedPrivateRoute; 23 | -------------------------------------------------------------------------------- /src/assets/images/img_empty_profile_small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/presentation/pages/Neoga/Result/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import { COLOR } from '@styles/common/color'; 4 | 5 | export const StNeogaResult = styled.div` 6 | min-height: 100vh; 7 | padding: 0 20px; 8 | padding-bottom: 58px; 9 | 10 | h1 { 11 | ${FONT_STYLES.SB_24_TITLE}; 12 | color: ${COLOR.GRAY_8}; 13 | font-weight: 600; 14 | margin-top: 12px; 15 | margin-bottom: 12px; 16 | } 17 | 18 | h2 { 19 | ${FONT_STYLES.R_15_TITLE}; 20 | color: ${COLOR.GRAY_5}; 21 | margin-bottom: 32px; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /.github/workflows/comment-pr.yml: -------------------------------------------------------------------------------- 1 | name: Comment on Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: [dev] 6 | paths-ignore: ['**.md'] 7 | 8 | jobs: 9 | example_comment_pr: 10 | runs-on: ubuntu-latest 11 | name: Comment on Pull Request 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | 16 | - name: Comment PR 17 | uses: thollander/actions-comment-pull-request@v1 18 | 19 | with: 20 | message: '울 웹쁜이 고생많았어 ! 여기서 미리보기로 보면서 쉬어~ 다른 웹쁜이들한테도 자랑해줘~' 21 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 22 | -------------------------------------------------------------------------------- /src/application/hooks/queries/neososeo-form.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | import { useQuery, UseQueryOptions } from 'react-query'; 3 | 4 | import { api } from '@api/index'; 5 | import { NeososeoFormData } from '@api/types/neososeo-form'; 6 | 7 | export const useGetFormInfo = ( 8 | q: string, 9 | options?: Omit< 10 | UseQueryOptions, 11 | 'queryKey' | 'queryFn' 12 | >, 13 | ) => 14 | useQuery(['neososeoForm', q], () => api.neososeoFormService.getFormInfo(q), { 15 | ...options, 16 | useErrorBoundary: options?.useErrorBoundary ?? true, 17 | }); 18 | -------------------------------------------------------------------------------- /src/infrastructure/api/types/neososeo-form.ts: -------------------------------------------------------------------------------- 1 | export type Relation = { 2 | id: number; 3 | content: string; 4 | }; 5 | 6 | export type NeososeoFormData = { 7 | title: string; 8 | content: string; 9 | imageSub: string; 10 | relation: Relation[]; 11 | userName: string; 12 | userID: number; 13 | userProfileImage?: string; 14 | createdID: number; 15 | answerCount: number; 16 | createdAt: string; 17 | formID: number; 18 | }; 19 | 20 | export type NeososeoAnswerData = { 21 | userID: number; 22 | formID: number; 23 | name: string; 24 | relationID: number; 25 | answer: string; 26 | keyword: string[]; 27 | }; 28 | -------------------------------------------------------------------------------- /src/presentation/components/common/ProfileAddButton/style.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { PROFILE_ADD_BUTTON } from '@styles/common/button'; 3 | 4 | export const StAddButton = styled.button<{ isSquare: boolean }>` 5 | ${PROFILE_ADD_BUTTON} 6 | 7 | width: ${(props) => (props.isSquare ? '60px' : '48px')}; 8 | height: ${(props) => (props.isSquare ? '60px' : '48px')}; 9 | border-radius: ${(props) => (props.isSquare ? '22px' : '50%')}; 10 | 11 | ${(props) => 12 | props.isSquare && 13 | css` 14 | & > img { 15 | width: 12px; 16 | height: 12px; 17 | } 18 | `} 19 | `; 20 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/Feedback/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StFeedbackEmptyView = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | padding: 44px 0; 11 | 12 | div:first-of-type { 13 | ${FONT_STYLES.SB_18_TITLE} 14 | color: ${COLOR.GRAY_4}; 15 | margin-bottom: 12px; 16 | } 17 | 18 | div:last-of-type { 19 | ${FONT_STYLES.M_14_TITLE} 20 | color: ${COLOR.GRAY_35}; 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/HomeTeam/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StHomeTeamEmptyView = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | margin-top: 60px; 10 | 11 | div:nth-of-type(1) { 12 | ${FONT_STYLES.SB_18_TITLE}; 13 | color: ${COLOR.GRAY_4}; 14 | font-weight: 600; 15 | margin-bottom: 12px; 16 | } 17 | 18 | div:nth-of-type(2) { 19 | ${FONT_STYLES.M_14_TITLE}; 20 | color: ${COLOR.GRAY_35}; 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /src/presentation/style/common/button.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | 4 | export const CORAL_MAIN_BUTTON = css` 5 | background-color: ${COLOR.CORAL_MAIN}; 6 | color: ${COLOR.WHITE}; 7 | `; 8 | 9 | export const FULL_WIDTH_BUTTON = css` 10 | width: 100%; 11 | min-height: 58px; 12 | border-radius: 14px; 13 | font-size: 16px; 14 | cursor: pointer; 15 | text-align: center; 16 | line-height: 58px; 17 | `; 18 | 19 | export const PROFILE_ADD_BUTTON = css` 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | background: ${COLOR.GRAY_1}; 24 | `; 25 | -------------------------------------------------------------------------------- /src/presentation/components/common/Toast/Item/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ANIMATION } from '@styles/common/animation'; 3 | 4 | export const StToastItem = styled.div<{ bottom?: number; isClosing: boolean }>` 5 | position: absolute; 6 | background-color: rgb(0, 0, 0, 0.5); 7 | height: 40px; 8 | border-radius: 20px; 9 | text-align: center; 10 | line-height: 40px; 11 | color: white; 12 | width: 100%; 13 | max-width: 350px; 14 | margin: 0 auto; 15 | bottom: ${({ bottom }) => bottom ?? 26}px; 16 | animation: 0.3s forwards 17 | ${({ isClosing }) => (isClosing ? ANIMATION.FADE_OUT : ANIMATION.FADE_IN)}; 18 | `; 19 | -------------------------------------------------------------------------------- /src/presentation/components/NeososeoAnswerCard/List/index.tsx: -------------------------------------------------------------------------------- 1 | import { AnswerDetail, MyDetail } from '@api/types/user'; 2 | import NeososeoAnswerCardItem from '../Item'; 3 | 4 | interface NeososeoAnswerCardListProps { 5 | answers: AnswerDetail[]; 6 | selectedForm?: MyDetail | null; 7 | } 8 | 9 | function NeososeoAnswerCardList(props: NeososeoAnswerCardListProps) { 10 | const { answers, selectedForm } = props; 11 | return ( 12 | <> 13 | {answers.map((answer) => ( 14 | 15 | ))} 16 | > 17 | ); 18 | } 19 | 20 | export default NeososeoAnswerCardList; 21 | -------------------------------------------------------------------------------- /src/presentation/components/common/ExpandListButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { IcArrowViewMore, IcArrowViewMoreClose } from '@assets/icons'; 2 | import { StExpandListButton } from './style'; 3 | 4 | type ExpandListButtonProps = { 5 | onClick: () => void; 6 | isExpanded: boolean; 7 | }; 8 | 9 | function ExpandListButton(props: ExpandListButtonProps) { 10 | const { onClick, isExpanded } = props; 11 | return ( 12 | 13 | {isExpanded ? '접기' : '더보기'} 14 | {isExpanded ? : } 15 | 16 | ); 17 | } 18 | 19 | export default ExpandListButton; 20 | -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@assets/*": ["src/assets/*"], 6 | "@components/*": ["src/presentation/components/*"], 7 | "@routes/*": ["src/presentation/routes/*"], 8 | "@pages/*": ["src/presentation/pages/*"], 9 | "@styles/*": ["src/presentation/style/*"], 10 | "@utils/*": ["src/application/utils/*"], 11 | "@models/*": ["src/application/models/*"], 12 | "@stores/*": ["src/application/stores/*"], 13 | "@hooks/*": ["src/application/hooks/*"], 14 | "@api/*": ["src/infrastructure/api/*"], 15 | "@infrastructure/*": ["src/infrastructure/*"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/presentation/components/TeamMemberAdd/ForEdit/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StTeamMemberAdd = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | width: 100%; 8 | min-height: 100vh; 9 | background-color: white; 10 | z-index: 100; 11 | & > *:first-child { 12 | margin-bottom: 20px; 13 | } 14 | & > *:nth-child(2) { 15 | & > *:first-child { 16 | margin-top: 18px; 17 | margin-bottom: 15px; 18 | } 19 | } 20 | & > *:last-child { 21 | margin-top: 18px; 22 | } 23 | `; 24 | 25 | export const StPaddingWrapper = styled.div` 26 | width: 100%; 27 | padding: 0 20px; 28 | `; 29 | -------------------------------------------------------------------------------- /src/presentation/components/TeamMemberAdd/ForRegister/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StTeamMemberAdd = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | width: 100%; 8 | min-height: 100vh; 9 | background-color: white; 10 | z-index: 100; 11 | & > *:first-child { 12 | margin-bottom: 20px; 13 | } 14 | & > *:nth-child(2) { 15 | & > *:first-child { 16 | margin-top: 18px; 17 | margin-bottom: 15px; 18 | } 19 | } 20 | & > *:last-child { 21 | margin-top: 18px; 22 | } 23 | `; 24 | 25 | export const StPaddingWrapper = styled.div` 26 | width: 100%; 27 | padding: 0 20px; 28 | `; 29 | -------------------------------------------------------------------------------- /src/presentation/pages/NeososeoForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { useParams, Outlet } from 'react-router-dom'; 2 | import GoogleAdsense from '@components/GoogleAdsense'; 3 | 4 | import { useGetFormInfo } from '@hooks/queries/neososeo-form'; 5 | import { StNeososeoFormPage } from './style'; 6 | 7 | function NeososeoFormPage() { 8 | const { q } = useParams(); 9 | const { data: neososeoFormData, isLoading } = useGetFormInfo(q ?? ''); 10 | 11 | return ( 12 | 13 | 14 | {!isLoading && neososeoFormData !== undefined && } 15 | 16 | ); 17 | } 18 | 19 | export default NeososeoFormPage; 20 | -------------------------------------------------------------------------------- /src/presentation/components/common/IssueTeamInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { StIssueTeamInfo } from './style'; 2 | import { imgEmptyProfile } from '@assets/images/index'; 3 | 4 | interface IssueTeamInfoProps { 5 | teamImage?: string; 6 | teamName: string; 7 | memberName: string; 8 | } 9 | 10 | function IssueTeamInfo(props: IssueTeamInfoProps) { 11 | const { teamImage, teamName, memberName } = props; 12 | return ( 13 | 14 | {teamImage ? : } 15 | {teamName} 16 | | 17 | {memberName} 18 | 19 | ); 20 | } 21 | 22 | export default IssueTeamInfo; 23 | -------------------------------------------------------------------------------- /src/presentation/routes/PreferencesRouter.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router-dom'; 2 | import { lazy } from 'react'; 3 | const Preferences = lazy(() => import('@pages/Preferences/index')); 4 | const ServiceCenter = lazy(() => import('@pages/Preferences/ServiceCenter')); 5 | const PrivateRoute = lazy(() => import('./common/PrivateRoute')); 6 | 7 | function PreferencesRouter() { 8 | return ( 9 | 10 | }> 11 | } /> 12 | } /> 13 | 14 | 15 | ); 16 | } 17 | 18 | export default PreferencesRouter; 19 | -------------------------------------------------------------------------------- /src/assets/lottie/TeamLottie/index.tsx: -------------------------------------------------------------------------------- 1 | import lottie from 'lottie-web'; 2 | import NeogaLottie from './TeamLandingLottie.json'; 3 | import { useEffect, useRef } from 'react'; 4 | 5 | function TeamLandingLottie() { 6 | const containerRef = useRef(null); 7 | useEffect(() => { 8 | const container = containerRef.current; 9 | if (container) 10 | lottie.loadAnimation({ 11 | container, 12 | renderer: 'svg', 13 | loop: true, 14 | autoplay: true, 15 | animationData: NeogaLottie, 16 | }); 17 | }, []); 18 | 19 | return ; 20 | } 21 | 22 | export default TeamLandingLottie; 23 | -------------------------------------------------------------------------------- /src/presentation/components/common/ModalWrapper/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StModalWrapper = styled.div<{ isOpened: boolean; isAnimation: boolean }>` 4 | width: 100%; 5 | min-height: 100vh; 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | visibility: ${(props) => (props.isOpened ? 'visible' : 'hidden')}; 10 | z-index: 999; 11 | background-color: rgba(0, 0, 0, 0.6); 12 | & > *:last-child { 13 | position: absolute; 14 | top: 50%; 15 | left: 50%; 16 | transform: ${(props) => (props.isOpened ? 'translate(-50%, -50%)' : 'translate(-50%, 0)')}; 17 | transition: ${(props) => (props.isAnimation ? 'all 0.3s' : 'initial')}; 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/presentation/style/common/color.ts: -------------------------------------------------------------------------------- 1 | export const COLOR = { 2 | CORAL_MAIN: '#FF6262', 3 | BLACK: '#000000', 4 | WHITE: '#FFFFFF', 5 | PINK: '#FF4B77', 6 | YELLOW: '#FFB72B', 7 | BLUE: '#4C48FF', 8 | PINK_LIGHT_SUB: '#FF5D84', 9 | CORAL_SUB: '#FF806F', 10 | CORAL_1: '#FFF0F0', 11 | GREEN_LIGHT_SUB: '#41E897', 12 | GREEN_SUB: '#3ACE90', 13 | SKYBLUE_SUB: '#42A4FF', 14 | PURPLE_SUB: '#8E58FF', 15 | GRAY_SUB: '#444444', 16 | GRAY_1: '#F8F8F8', 17 | GRAY_15: '#F5F5F5', 18 | GRAY_2: '#EFEFEF', 19 | GRAY_3: '#E9E9E9', 20 | GRAY_35: '#DEDEDE', 21 | GRAY_4: '#BEBEBE', 22 | GRAY_5: '#909090', 23 | GRAY_6: '#606060', 24 | GRAY_7: '#434343', 25 | GRAY_8: '#2B2B2B', 26 | }; 27 | -------------------------------------------------------------------------------- /src/application/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const isAllFilled = (...args: unknown[]) => 2 | args.every((arg) => arg !== null && arg !== undefined && arg !== ''); 3 | 4 | export const splitIntoTwoLines = (oneLineString: string): string => { 5 | const splittedString = oneLineString.split(' '); 6 | let firstLine = '', 7 | secondLine = ''; 8 | while (splittedString.length) { 9 | if (firstLine.length < secondLine.length) firstLine += ' ' + splittedString.shift(); 10 | else secondLine = splittedString.pop() + ' ' + secondLine; 11 | } 12 | return firstLine + '\n' + secondLine; 13 | }; 14 | 15 | export const removeSpecialCharacters = (title: string) => 16 | title.replace('\\n', '\n').replaceAll('*', ''); 17 | -------------------------------------------------------------------------------- /src/assets/icons/ic_new_tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/application/hooks/useToast.ts: -------------------------------------------------------------------------------- 1 | import { useRecoilState } from 'recoil'; 2 | 3 | import { Toast, toastState } from '@stores/toast'; 4 | import { getRandomID } from '@utils/etc'; 5 | 6 | export function useToast() { 7 | const [toasts, setToasts] = useRecoilState(toastState); 8 | 9 | const removeToast = (toastID: Toast['id']) => { 10 | setToasts((prev) => prev.filter((toast) => toast.id !== toastID)); 11 | }; 12 | 13 | const fireToast = (toast: Toast) => { 14 | const toastID = getRandomID(); 15 | setToasts((prev) => [...prev, { ...toast, id: toastID, duration: toast.duration ?? 1000 }]); 16 | setTimeout(() => removeToast(toastID), 600 + (toast.duration ?? 1000)); 17 | }; 18 | 19 | return { toasts, fireToast }; 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/icons/ic_camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/lottie/NeogaLottie/index.tsx: -------------------------------------------------------------------------------- 1 | import lottie from 'lottie-web'; 2 | import NeogaLottie from './NeogaLandingLottie.json'; 3 | import { useEffect } from 'react'; 4 | 5 | function NeogaLandingLottie() { 6 | useEffect(() => { 7 | const component = document.querySelector('#neogaContainer'); 8 | component && 9 | lottie.loadAnimation({ 10 | container: component, 11 | renderer: 'svg', 12 | loop: true, 13 | autoplay: true, 14 | animationData: NeogaLottie, 15 | }); 16 | }, []); 17 | return ( 18 | 22 | ); 23 | } 24 | 25 | export default NeogaLandingLottie; 26 | -------------------------------------------------------------------------------- /src/presentation/components/common/Toast/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import { Toast } from '@stores/toast'; 2 | import { useEffect, useState } from 'react'; 3 | import { StToastItem } from './style'; 4 | 5 | function ToastItem(props: Toast) { 6 | const { content, bottom, duration } = props; 7 | const [isClosing, setIsClosing] = useState(false); 8 | 9 | useEffect(() => { 10 | const setExistTimeout = setTimeout(() => { 11 | setIsClosing(true); 12 | clearTimeout(setExistTimeout); 13 | }, duration ?? 1000); 14 | 15 | return () => clearTimeout(setExistTimeout); 16 | }, []); 17 | 18 | return ( 19 | 20 | {content} 21 | 22 | ); 23 | } 24 | 25 | export default ToastItem; 26 | -------------------------------------------------------------------------------- /src/presentation/pages/Home/Team/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | 4 | export const StTeamMain = styled.div` 5 | height: 100vh; 6 | overflow-x: hidden; 7 | -ms-overflow-style: none; 8 | scrollbar-width: none; 9 | &::-webkit-scrollbar { 10 | display: none; 11 | } 12 | 13 | & h1 { 14 | font-weight: 600; 15 | font-size: 18px; 16 | color: ${COLOR.GRAY_8}; 17 | margin-top: 28px; 18 | margin-bottom: 18px; 19 | padding: 0 20px; 20 | } 21 | 22 | & > div { 23 | padding: 0 20px; 24 | } 25 | `; 26 | 27 | export const StDivisionLine = styled.div` 28 | width: 100%; 29 | height: 8px; 30 | background-color: ${COLOR.GRAY_1}; 31 | margin-top: 24px; 32 | `; 33 | -------------------------------------------------------------------------------- /src/presentation/pages/NeososeoForm/Finish/index.tsx: -------------------------------------------------------------------------------- 1 | import { ImgAnswerDone } from '@assets/images'; 2 | import GoogleAdsense from '@components/GoogleAdsense'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { StButton } from '../style'; 5 | import { StBody, StNeososeoFinish } from './style'; 6 | 7 | function NeososeoFormFinish() { 8 | const navigate = useNavigate(); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 답변이 전달되었어요 16 | 내 너가소개서도 받아보세요. 17 | 18 | navigate('/')}>내 너가소개서도 받아보기 19 | 20 | ); 21 | } 22 | 23 | export default NeososeoFormFinish; 24 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/UserSearch/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StUserSearchEmptyView = styled.div` 7 | width: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | & > *:first-child { 13 | margin-top: 44px; 14 | ${FONT_STYLES.SB_18_TITLE} 15 | line-height: 100%; 16 | letter-spacing: -0.01em; 17 | color: ${COLOR.GRAY_4}; 18 | } 19 | & > *:last-child { 20 | margin-top: 12px; 21 | ${FONT_STYLES.M_14_TITLE} 22 | line-height: 100%; 23 | letter-spacing: -0.01em; 24 | color: ${COLOR.GRAY_35}; 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/assets/icons/ic_unpicked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaCreateCard/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import { IcArrowRight } from '@assets/icons'; 2 | import { StNeogaCreateCardItem } from './style'; 3 | 4 | interface NeogaCreateCardItemProps { 5 | idx: number; 6 | title: string; 7 | content: string; 8 | src: string; 9 | onClick: () => void; 10 | } 11 | 12 | function NeogaCreateCardItem(props: NeogaCreateCardItemProps) { 13 | const { title, content, src, onClick, idx } = props; 14 | return ( 15 | 16 | 17 | 18 | {title} 19 | {content} 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default NeogaCreateCardItem; 27 | -------------------------------------------------------------------------------- /src/presentation/components/common/FormItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { StErrorMsg, StFormItem, StInputStatus, StCount } from './style'; 4 | 5 | interface FormItemProps { 6 | children: ReactNode; 7 | value: string; 8 | errorMsg?: string; 9 | maxLength?: number; 10 | } 11 | 12 | export default function FormItem(props: FormItemProps) { 13 | const { errorMsg, children, value, maxLength } = props; 14 | return ( 15 | 16 | {children} 17 | 18 | {errorMsg && errorMsg} 19 | {maxLength && ( 20 | {`${value.length}/${maxLength}`} 21 | )} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/presentation/components/common/Label/index.tsx: -------------------------------------------------------------------------------- 1 | import { StCommonLabel, StLabel, StLabelWithOptional, StOptional } from './style'; 2 | 3 | interface CommonLabelProps { 4 | content: string; 5 | marginBottom?: string; 6 | marginTop?: string; 7 | isOptional?: boolean; 8 | } 9 | 10 | export default function CommonLabel(props: CommonLabelProps) { 11 | const { content, marginBottom, marginTop, isOptional } = props; 12 | return isOptional ? ( 13 | 14 | {content} 15 | (선택) 16 | 17 | ) : ( 18 | 19 | {content} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/icons/ic_picked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/img_empty_join.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/MyPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { StButton, StLabel, StMyEmptyView } from './style'; 2 | 3 | type MyEmptyViewProps = { 4 | isMyPage: boolean; 5 | origin: string; 6 | pickTarget: string; 7 | onPickButtonClicked: () => void; 8 | }; 9 | 10 | function MyEmptyView(props: MyEmptyViewProps) { 11 | const { isMyPage, origin, pickTarget, onPickButtonClicked } = props; 12 | return ( 13 | 14 | 픽한 {pickTarget}이 없어요 15 | {isMyPage && ( 16 | 17 | {origin}에서 받은 {pickTarget}들 중 18 | 마음에 드는 {pickTarget}을 픽해보세요 19 | 20 | )} 21 | {isMyPage && {origin} 픽 하러 가기} 22 | 23 | ); 24 | } 25 | 26 | export default MyEmptyView; 27 | -------------------------------------------------------------------------------- /src/presentation/components/MyItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { StMyItem } from './style'; 2 | import { imgEmptyProfile } from '@assets/images/index'; 3 | 4 | interface MyItemProps { 5 | id: number; 6 | title?: string; 7 | profileImage?: string; 8 | isSquare: boolean; 9 | isSelected?: boolean | undefined; 10 | onProfileClick: (id: number) => void; 11 | } 12 | 13 | function MyItem(props: MyItemProps) { 14 | const { id, title, profileImage, isSquare, onProfileClick, isSelected } = props; 15 | 16 | return ( 17 | onProfileClick(id)} 22 | > 23 | 24 | {isSquare && {title}} 25 | 26 | ); 27 | } 28 | 29 | export default MyItem; 30 | -------------------------------------------------------------------------------- /src/presentation/components/common/FormItem/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StFormItem = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | `; 10 | 11 | export const StInputStatus = styled.div` 12 | display: flex; 13 | justify-content: space-between; 14 | `; 15 | 16 | export const StErrorMsg = styled.div` 17 | padding-left: 4px; 18 | padding-top: 15px; 19 | color: ${COLOR.CORAL_MAIN}; 20 | ${FONT_STYLES.R_14_TITLE}; 21 | `; 22 | 23 | export const StCount = styled.div<{ isMax: boolean }>` 24 | padding-right: 6px; 25 | padding-top: 6px; 26 | color: ${(props) => (props.isMax ? COLOR.CORAL_MAIN : COLOR.GRAY_4)}; 27 | ${FONT_STYLES.R_13_TITLE}; 28 | `; 29 | -------------------------------------------------------------------------------- /src/presentation/components/common/IssueTeamInfo/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | 4 | export const StIssueTeamInfo = styled.div` 5 | width: fit-content; 6 | border: 1px solid ${COLOR.GRAY_2}; 7 | border-radius: 10px; 8 | padding: 5px 7px; 9 | display: flex; 10 | align-items: center; 11 | font-size: 12px; 12 | 13 | & > * + * { 14 | margin-left: 6px; 15 | } 16 | 17 | & > img { 18 | width: 22px; 19 | height: 22px; 20 | border-radius: 8px; 21 | object-fit: cover; 22 | } 23 | 24 | span { 25 | color: ${COLOR.GRAY_4}; 26 | font-size: 12px; 27 | line-height: 22px; 28 | letter-spacing: -0.01em; 29 | } 30 | 31 | span:first-of-type { 32 | color: ${COLOR.GRAY_7}; 33 | font-weight: 600; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/application/hooks/queries/team.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient, useMutation, UseMutationOptions } from 'react-query'; 2 | 3 | import { api } from '@api/index'; 4 | import { TeamProfileData } from '@api/types/team'; 5 | 6 | const queryClient = new QueryClient(); 7 | 8 | export const useDeleteTeam = (teamID: number) => 9 | useMutation(async (teamID: number) => await api.teamService.deleteTeam(teamID), { 10 | onSuccess: () => { 11 | queryClient.setQueryData('teamProfileData', (old: TeamProfileData | undefined) => { 12 | return { profileList: old ? old.profileList.filter((o) => o.id !== teamID) : [] }; 13 | }); 14 | }, 15 | }); 16 | 17 | export const usePickTeamFeedback = (feedbackID: number, options?: UseMutationOptions) => 18 | useMutation(async () => await api.teamService.postFeedbackBookmark(feedbackID), options); 19 | -------------------------------------------------------------------------------- /src/assets/icons/ic_lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/presentation/pages/NeososeoForm/Finish/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StBody = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | padding-bottom: 2px; 11 | & > div:nth-child(2) { 12 | color: ${COLOR.GRAY_8}; 13 | ${FONT_STYLES.SB_20_TITLE} 14 | } 15 | & > div:nth-child(3) { 16 | margin-top: 8px; 17 | color: ${COLOR.GRAY_5}; 18 | ${FONT_STYLES.R_15_TITLE} 19 | } 20 | `; 21 | 22 | export const StNeososeoFinish = styled.div` 23 | height: 100vh; 24 | width: 100%; 25 | padding: 0 20px 50px 20px; 26 | display: grid; 27 | grid-template-rows: auto 58px; 28 | position: relative; 29 | `; 30 | -------------------------------------------------------------------------------- /src/infrastructure/mock/neososeo-form.ts: -------------------------------------------------------------------------------- 1 | import { NeososeoFormService } from '@api/neososeo-form'; 2 | import { NEOSOSEO_FORM_DATA } from './neososeo-form.data'; 3 | 4 | export function NeososeoFormMock(): NeososeoFormService { 5 | const getFormInfo = async () => { 6 | await wait(2000); 7 | const form = NEOSOSEO_FORM_DATA.FORM; 8 | const userName = form.userName; 9 | return { 10 | ...form, 11 | title: form.title.replaceAll('{{user}}', userName), 12 | content: form.content.replaceAll('{{user}}', userName), 13 | }; 14 | }; 15 | 16 | const postFormAnswer = async () => { 17 | await wait(2000); 18 | return { isSuccess: true }; 19 | }; 20 | 21 | return { getFormInfo, postFormAnswer }; 22 | } 23 | 24 | const wait = (milliSeconds: number) => new Promise((resolve) => setTimeout(resolve, milliSeconds)); 25 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/TeamProfile/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import StSkeletonItem from '../style'; 4 | import { COLOR } from '@styles/common/color'; 5 | 6 | export const StTeamProfileTitle = styled(StSkeletonItem)` 7 | width: 140px; 8 | height: 16px; 9 | background-color: ${COLOR.GRAY_3}; 10 | margin: 39px 0px 16px 20px; 11 | `; 12 | 13 | export const StTeamProfileContainer = styled.div` 14 | display: flex; 15 | gap: 18px; 16 | margin-bottom: 33px; 17 | `; 18 | 19 | export const StTeamImage = styled(StSkeletonItem)` 20 | width: 60px; 21 | height: 60px; 22 | margin-bottom: 6px; 23 | background-color: ${COLOR.GRAY_2}; 24 | `; 25 | 26 | export const StTeamName = styled(StSkeletonItem)` 27 | width: 60px; 28 | height: 12px; 29 | background-color: ${COLOR.GRAY_2}; 30 | `; 31 | -------------------------------------------------------------------------------- /src/presentation/components/common/ProfileItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { StProfileItem } from './style'; 2 | import { imgEmptyProfile } from '@assets/images/index'; 3 | 4 | interface ProfileItemProps { 5 | id: number; 6 | profileImage?: string; 7 | profileName: string; 8 | isSquare: boolean; 9 | isSelected?: boolean | undefined; 10 | onProfileClick: (id: number) => void; 11 | } 12 | 13 | function ProfileItem(props: ProfileItemProps) { 14 | const { id, profileImage, profileName, isSquare, onProfileClick, isSelected } = props; 15 | 16 | return ( 17 | onProfileClick(id)}> 18 | 19 | 20 | 21 | {profileName} 22 | 23 | ); 24 | } 25 | 26 | export default ProfileItem; 27 | -------------------------------------------------------------------------------- /src/assets/images/img_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaFormTicket/index.tsx: -------------------------------------------------------------------------------- 1 | import { StCircle, StNeogaFormTicket } from './style'; 2 | 3 | interface NeogaFormTicketProps { 4 | image: string; 5 | title: string; 6 | content: string; 7 | children: React.ReactNode; 8 | theme?: 'WHITE' | 'CORAL'; 9 | isSmall?: boolean; 10 | } 11 | 12 | export default function NeogaFormTicket(props: NeogaFormTicketProps) { 13 | const { image, title, content, children, theme = 'WHITE', isSmall = false } = props; 14 | return ( 15 | 16 | {image && } 17 | {content} 18 | {title} 19 | 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/presentation/components/NeososeoFormHeader/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StNeososeoFormHeader = styled.div` 6 | width: 100%; 7 | margin: 0 auto; 8 | word-break: keep-all; 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-between; 12 | 13 | & div { 14 | line-height: 143.99%; 15 | color: ${COLOR.GRAY_8}; 16 | ${FONT_STYLES.SB_22_BODY} 17 | } 18 | 19 | & div:first-child { 20 | flex: 1.4; 21 | white-space: pre-line; 22 | } 23 | 24 | & div:last-child { 25 | flex: 0.6; 26 | } 27 | 28 | & img { 29 | width: 68px; 30 | height: 68px; 31 | border-radius: 34px; 32 | background-color: ${COLOR.GRAY_1}; 33 | float: right; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/presentation/pages/Home/MyPage/Keyword/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StMyKeyword = styled.div` 7 | position: relative; 8 | `; 9 | 10 | export const StMyKeywordHeader = styled.div` 11 | height: 76px; 12 | background-color: ${COLOR.GRAY_1}; 13 | padding: 42px 20px 19px 24px; 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | 18 | div { 19 | ${FONT_STYLES.SB_15_TITLE}; 20 | color: ${COLOR.GRAY_6}; 21 | } 22 | 23 | span:last-child { 24 | margin-left: 3px; 25 | color: ${COLOR.CORAL_MAIN}; 26 | } 27 | 28 | svg { 29 | cursor: pointer; 30 | } 31 | `; 32 | 33 | export const StLoaderWrapper = styled.div` 34 | margin-top: 50px; 35 | `; 36 | -------------------------------------------------------------------------------- /src/presentation/style/common/modal.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | import { COLOR } from './color'; 4 | import { FONT_STYLES } from './font-style'; 5 | 6 | export const COMMON_MODAL_BUTTON = css` 7 | display: flex; 8 | & > button { 9 | width: 144px; 10 | height: 50px; 11 | border-radius: 12px; 12 | ${FONT_STYLES.R_15_BODY}; 13 | line-height: 100%; 14 | letter-spacing: -0.01em; 15 | } 16 | & > button:first-child { 17 | margin-right: 10px; 18 | background-color: ${COLOR.GRAY_15}; 19 | color: ${COLOR.GRAY_5}; 20 | } 21 | & > button:last-child { 22 | background-color: ${COLOR.CORAL_1}; 23 | color: ${COLOR.CORAL_MAIN}; 24 | } 25 | `; 26 | 27 | export const COMMON_MODAL = css` 28 | background-color: #ffffff; 29 | box-shadow: 0px 0px 24px rgba(0, 0, 0, 0.08); 30 | border-radius: 24px; 31 | `; 32 | -------------------------------------------------------------------------------- /src/assets/icons/ic_trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/img_logo_header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/presentation/components/JoinCompleteForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { StJoinCompleteForm, StButton, StNoticeWrapper } from './style'; 2 | import { imgParty } from '@assets/images'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useLoginUser } from '@hooks/useLoginUser'; 5 | 6 | const JoinCompleteForm = () => { 7 | const navigate = useNavigate(); 8 | const { username } = useLoginUser(); 9 | 10 | return ( 11 | 12 | 13 | 14 | {username}님 환영합니다! 15 | 회원가입이 완료되었어요 16 | 17 | 이제 내 너가소개서를 받아보세요 18 | navigate('/home')}> 19 | 확인 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default JoinCompleteForm; 26 | -------------------------------------------------------------------------------- /src/assets/icons/ic_camera_main_coral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/NeogaDetailForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from '@hooks/useToast'; 2 | import { copyClipboard } from '@utils/copyClipboard'; 3 | import { StNeogaDetailFormEmptyView } from './style'; 4 | 5 | interface NeogaDetailFormEmptyViewProps { 6 | link: string; 7 | } 8 | 9 | function NeogaDetailFormEmptyView(props: NeogaDetailFormEmptyViewProps) { 10 | const { link } = props; 11 | const { fireToast } = useToast(); 12 | 13 | return ( 14 | 15 | 아직 답변이 없어요. 16 | 링크를 공유하고 답변을 받아보세요. 17 | 19 | copyClipboard(link, () => fireToast({ content: '링크가 클립보드에 저장되었습니다.' })) 20 | } 21 | > 22 | 링크 복사하기 23 | 24 | 25 | ); 26 | } 27 | 28 | export default NeogaDetailFormEmptyView; 29 | -------------------------------------------------------------------------------- /src/presentation/components/common/Modal/DeleteIssue/index.tsx: -------------------------------------------------------------------------------- 1 | import { api } from '@api/index'; 2 | import { useNavigate } from 'react-router'; 3 | import CommonModal from '..'; 4 | 5 | type DeleteIssueModalProps = { 6 | isOpened: boolean; 7 | closeModal(): void; 8 | issueID: number; 9 | }; 10 | 11 | function DeleteIssueModal(props: DeleteIssueModalProps) { 12 | const { closeModal, issueID, isOpened } = props; 13 | const navigate = useNavigate(); 14 | const deleteIssue = async () => { 15 | const response = await api.teamService.deleteIssue(issueID); 16 | if (response.isSuccess) { 17 | navigate(-1); 18 | } 19 | }; 20 | return ( 21 | 27 | ); 28 | } 29 | 30 | export default DeleteIssueModal; 31 | -------------------------------------------------------------------------------- /src/presentation/pages/Join/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | import { CORAL_MAIN_BUTTON } from '@styles/common/button'; 6 | 7 | export const StJoin = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | margin: 0 20px; 12 | 13 | h1 { 14 | ${FONT_STYLES.SB_20_TITLE}; 15 | color: ${COLOR.GRAY_8}; 16 | margin: 54px 0px 72px 0px; 17 | } 18 | 19 | & > *:not(& > *:nth-child(2)) { 20 | width: 100%; 21 | } 22 | `; 23 | 24 | export const StButton = styled.button` 25 | height: 58px; 26 | margin-top: 137px; 27 | margin-bottom: 48px; 28 | border-radius: 18px; 29 | ${CORAL_MAIN_BUTTON}; 30 | ${FONT_STYLES.M_16_TITLE}; 31 | :disabled { 32 | background-color: ${COLOR.GRAY_3}; 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /src/assets/icons/ic_empty_keyword.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/NSSPick/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StTitle, 3 | StHeader, 4 | StImage, 5 | StNssTitle, 6 | StButton, 7 | StBody, 8 | StContent, 9 | StKeyword, 10 | StKeywordContainer, 11 | } from './style'; 12 | 13 | function NSSPickSkeleton() { 14 | return ( 15 | <> 16 | 17 | {new Array(2).fill('').map((_, i) => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ))} 34 | > 35 | ); 36 | } 37 | 38 | export default NSSPickSkeleton; 39 | -------------------------------------------------------------------------------- /src/presentation/pages/Home/MyPage/TeamPick/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StMyTeamPick = styled.div` 7 | header { 8 | background-color: ${COLOR.GRAY_1}; 9 | height: 76px; 10 | padding: 18px 20px; 11 | ${FONT_STYLES.R_14_BODY}; 12 | color: ${COLOR.GRAY_6}; 13 | line-height: 140%; 14 | 15 | span { 16 | color: ${COLOR.CORAL_MAIN}; 17 | } 18 | } 19 | `; 20 | 21 | export const StMyTeamList = styled.div` 22 | & > div:first-child { 23 | padding: 24px 23px 0 23px; 24 | } 25 | 26 | & > div:last-child { 27 | padding: 37px 23px 0 23px; 28 | color: ${COLOR.GRAY_8}; 29 | ${FONT_STYLES.SB_18_TITLE}; 30 | } 31 | `; 32 | 33 | export const StMyTeamPickList = styled.div` 34 | padding: 0 20px; 35 | `; 36 | -------------------------------------------------------------------------------- /src/presentation/pages/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import { KAKAO_AUTH_URL } from './OAuth'; 2 | import { StLogin, StLoginImg, StLoginButton, StTitle, StNoticeWrapper } from './style'; 3 | import { icKakao } from '@assets/icons/index'; 4 | import { imgLogo, imgLoginCharacter } from '@assets/images/index'; 5 | 6 | export default function Login() { 7 | const loginWithKakao = () => { 8 | window.location.href = KAKAO_AUTH_URL; 9 | }; 10 | 11 | return ( 12 | 13 | 14 | 너가소개서 15 | 16 | 나와 함께한 사람들이 써 주는 17 | 18 | 나의 소개서, 너가소개서 19 | 20 | 21 | loginWithKakao()}> 22 | 23 | 카카오로 계속하기 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/application/hooks/useGoogleAnalytics.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import ReactGA from 'react-ga'; 3 | 4 | export const enum GaCategory { 5 | LANDING = 'landing', 6 | TSS = 'tss', 7 | NSS = 'nss', 8 | } 9 | 10 | export const enum GaAction { 11 | CLICK = 'click', 12 | } 13 | 14 | type GaKey = { 15 | category: GaCategory; 16 | action: GaAction; 17 | label?: string; 18 | }; 19 | 20 | export function useGoogleAnalytics() { 21 | const [isGoogleAnalyticsLoaded, setIsGoogleAnalyticsLoaded] = useState(false); 22 | 23 | const makeGaEvent = ({ category, action, label }: GaKey) => { 24 | if (!isGoogleAnalyticsLoaded) return; 25 | ReactGA.event({ category, action, label }); 26 | }; 27 | 28 | useEffect(() => { 29 | ReactGA.initialize('G-4DZZ8ZJJYS'); 30 | setIsGoogleAnalyticsLoaded(true); 31 | }, []); 32 | 33 | return { isGoogleAnalyticsLoaded, makeGaEvent }; 34 | } 35 | -------------------------------------------------------------------------------- /src/presentation/pages/Home/MyPage/NeogaPick/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StMyNeogaPick = styled.div` 7 | header { 8 | background-color: ${COLOR.GRAY_1}; 9 | height: 76px; 10 | padding: 18px 20px; 11 | ${FONT_STYLES.R_14_BODY}; 12 | color: ${COLOR.GRAY_6}; 13 | line-height: 140%; 14 | 15 | span { 16 | color: ${COLOR.CORAL_MAIN}; 17 | } 18 | } 19 | `; 20 | 21 | export const StMyNeogaFormList = styled.div` 22 | padding: 0 24px; 23 | 24 | & > div:first-child { 25 | padding-top: 26px; 26 | } 27 | 28 | & > div:last-child { 29 | padding-top: 36px; 30 | color: ${COLOR.GRAY_8}; 31 | ${FONT_STYLES.SB_18_TITLE}; 32 | } 33 | `; 34 | 35 | export const StMyNeogaPickList = styled.div` 36 | padding: 0 21px; 37 | `; 38 | -------------------------------------------------------------------------------- /src/presentation/components/common/Label/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | 4 | interface StLabelProps { 5 | marginTop?: string; 6 | marginBottom?: string; 7 | } 8 | 9 | export const StCommonLabel = styled.div` 10 | margin-bottom: ${(props) => props.marginBottom}; 11 | margin-top: ${(props) => props.marginTop}; 12 | `; 13 | 14 | export const StLabel = styled.div` 15 | font-weight: 600; 16 | font-size: 16px; 17 | color: ${COLOR.GRAY_7}; 18 | margin-bottom: ${(props) => props.marginBottom}; 19 | margin-top: ${(props) => props.marginTop}; 20 | `; 21 | 22 | export const StLabelWithOptional = styled.span` 23 | font-weight: 600; 24 | font-size: 16px; 25 | color: ${COLOR.GRAY_7}; 26 | `; 27 | 28 | export const StOptional = styled.span` 29 | font-size: 16px; 30 | color: ${COLOR.GRAY_5}; 31 | margin-left: 5px; 32 | `; 33 | -------------------------------------------------------------------------------- /src/infrastructure/mock/neososeo-form.data.ts: -------------------------------------------------------------------------------- 1 | import { NeososeoFormData } from '@api/types/neososeo-form'; 2 | 3 | export const NEOSOSEO_FORM_DATA: { FORM: NeososeoFormData } = { 4 | FORM: { 5 | title: '너가 닮고 싶은\n{{user}}의 일잘러 모습', 6 | content: '{{user}}와 함께하며 당신이 닮고 싶었던 능력이 있었나요?', 7 | relation: [ 8 | { id: 1, content: '동네친구' }, 9 | { id: 2, content: '쿵짝최고' }, 10 | { id: 3, content: '존경해요' }, 11 | { id: 4, content: '찐친베프' }, 12 | ], 13 | imageSub: 14 | 'https://ww.namu.la/s/61e3a8075b5aa238383c0d89badd3442f7389a0285575fc5bc5c16d2a34f22c66e57e35d568b63b706fa24750c784d55e972ceeea93fa5a91d00dab1eea37681e189ae826afe668fb379b0cb3a446f3268755691ed20c6a165185d44e2fd1029896062ed4df01a806594e35a637b2372', 15 | userName: '강쥐', 16 | userID: 1, 17 | formID: 1, 18 | createdID: 1, 19 | userProfileImage: '', 20 | answerCount: 0, 21 | createdAt: '2022-08-31', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/presentation/components/common/IssueMemberList/index.tsx: -------------------------------------------------------------------------------- 1 | import { TeamMemberNoneId } from '@api/types/team'; 2 | import { imgEmptyProfile } from '@assets/images'; 3 | import { StIssueMemberList } from './style'; 4 | 5 | interface IssueMemberListProps { 6 | teamID: string; 7 | issueNumber: number; 8 | issueMembers: TeamMemberNoneId[]; 9 | } 10 | 11 | function IssueMemberList(props: IssueMemberListProps) { 12 | const { issueMembers } = props; 13 | const length = issueMembers.length; 14 | const MAX_IMAGE_NUM = 3; 15 | 16 | return ( 17 | 18 | 19 | {issueMembers.slice(0, MAX_IMAGE_NUM).map(({ id, profileImage }) => ( 20 | 21 | ))} 22 | 23 | {length <= MAX_IMAGE_NUM ? null : +{length - MAX_IMAGE_NUM}} 24 | 25 | ); 26 | } 27 | 28 | export default IssueMemberList; 29 | -------------------------------------------------------------------------------- /src/presentation/components/common/IssueMemberList/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import { COLOR } from '@styles/common/color'; 4 | 5 | export const StIssueMemberList = styled.div` 6 | display: flex; 7 | align-items: center; 8 | 9 | & div { 10 | width: 73px; 11 | } 12 | 13 | & img { 14 | width: 32px; 15 | height: 32px; 16 | object-fit: cover; 17 | position: relative; 18 | border-radius: 50%; 19 | margin-right: -11px; 20 | } 21 | 22 | & img:not(img:nth-of-type(1)) { 23 | opacity: 0.7; 24 | } 25 | 26 | & img:nth-of-type(1) { 27 | z-index: 3; 28 | } 29 | 30 | & img:nth-of-type(2) { 31 | z-index: 2; 32 | } 33 | 34 | & img:nth-of-type(3) { 35 | z-index: 1; 36 | } 37 | 38 | & span { 39 | ${FONT_STYLES.M_14_TITLE}; 40 | color: ${COLOR.GRAY_7}; 41 | margin-left: 4px; 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /src/presentation/routes/NeogaRouter.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router-dom'; 2 | import { lazy } from 'react'; 3 | const NeogaCreate = lazy(() => import('@pages/Neoga/Create')); 4 | const NeogaResult = lazy(() => import('@pages/Neoga/Result')); 5 | const FormDetail = lazy(() => import('@pages/Neoga/FormDetail')); 6 | const NeogaLink = lazy(() => import('@pages/Neoga/Link')); 7 | const PrivateRoute = lazy(() => import('./common/PrivateRoute')); 8 | 9 | function NeogaRouter() { 10 | return ( 11 | 12 | }> 13 | } /> 14 | } /> 15 | } /> 16 | } /> 17 | 18 | 19 | ); 20 | } 21 | 22 | export default NeogaRouter; 23 | -------------------------------------------------------------------------------- /src/application/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const isProduction = process.env.NODE_ENV === 'production'; 2 | export const DOMAIN = isProduction ? 'https://naegasogaeseo-dev.kro.kr' : 'http://localhost:3000'; 3 | 4 | export const PAGES = { KEYWORD: 15, NOTICE: 15, PICK: 10, SEARCHED_USER: 16 }; 5 | 6 | export const SCROLL_BOTTOM_PADDING = 30; 7 | 8 | export const MAX_TEAM_MEMBER = 4; 9 | 10 | export const STATUS_CODE = { 11 | OK: 200, 12 | CREATED: 201, 13 | NO_CONTENT: 204, 14 | BAD_REQUEST: 400, 15 | UNAUTHORIZED: 401, 16 | FORBIDDEN: 403, 17 | NOT_FOUND: 404, 18 | INTERNAL_SERVER_ERROR: 500, 19 | SERVICE_UNAVAILABLE: 503, 20 | DB_ERROR: 600, 21 | }; 22 | 23 | export const TOKEN_KEYS = { ACCESS: 'accessToken', REFRESH: 'refreshToken' }; 24 | 25 | export const INITIAL_LOGIN_USER = { 26 | isJoined: false, 27 | accessToken: '', 28 | refreshToken: '', 29 | user: { id: -1, userID: '', username: '', profileImage: '' }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/presentation/routes/HomeRouter.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router-dom'; 2 | import { lazy } from 'react'; 3 | const HomeMyPage = lazy(() => import('@pages/Home/MyPage')); 4 | const HomeTeam = lazy(() => import('@pages/Home/Team')); 5 | const HomeNeoga = lazy(() => import('@pages/Home/Neoga')); 6 | const PrivateRoute = lazy(() => import('./common/PrivateRoute')); 7 | import PublicRoute from './common/PublicRoute'; 8 | 9 | function HomeRouter() { 10 | return ( 11 | 12 | }> 13 | } /> 14 | } /> 15 | } /> 16 | 17 | }> 18 | } /> 19 | 20 | 21 | ); 22 | } 23 | 24 | export default HomeRouter; 25 | -------------------------------------------------------------------------------- /src/presentation/components/JoinCompleteForm/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | 4 | export const StJoinCompleteForm = styled.div` 5 | text-align-last: center; 6 | min-height: 100vh; 7 | padding: 0 20px; 8 | & > img { 9 | margin-top: 150px; 10 | } 11 | & > p { 12 | margin-top: 21px; 13 | text-align: center; 14 | font-size: 15px; 15 | line-height: 100%; 16 | letter-spacing: -0.01em; 17 | color: ${COLOR.GRAY_5}; 18 | } 19 | `; 20 | 21 | export const StNoticeWrapper = styled.div` 22 | font-weight: 600; 23 | font-size: 20px; 24 | line-height: 140%; 25 | text-align: center; 26 | `; 27 | 28 | export const StButton = styled.button` 29 | margin-top: 204px; 30 | margin-bottom: 48px; 31 | width: 100%; 32 | height: 58px; 33 | color: white; 34 | border-radius: 18px; 35 | font-size: 16px; 36 | background-color: ${COLOR.CORAL_MAIN}; 37 | `; 38 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/MyPageInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StImage, 3 | StKeywordContainer, 4 | StKeyword, 5 | StLongText, 6 | StProfile, 7 | StShortText, 8 | StSubtitle, 9 | StMyKeyword, 10 | StTitle, 11 | } from './style'; 12 | 13 | function MyPageInfoSkeleton() { 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {new Array(2).fill('').map((_, i) => ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ))} 35 | 36 | > 37 | ); 38 | } 39 | 40 | export default MyPageInfoSkeleton; 41 | -------------------------------------------------------------------------------- /src/presentation/components/common/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { IcWarning } from '@assets/icons'; 2 | import ModalWrapper from '../ModalWrapper'; 3 | import { StCommonModal, StDescription } from './style'; 4 | 5 | interface CommonModalProps { 6 | onClickConfirm: () => void; 7 | onClickCancel: () => void; 8 | title: string; 9 | description?: string; 10 | isOpened: boolean; 11 | } 12 | 13 | export default function CommonModal(props: CommonModalProps) { 14 | const { onClickConfirm, onClickCancel, title, description, isOpened } = props; 15 | return ( 16 | 17 | 18 | 19 | {title} 20 | {description && {description}} 21 | 22 | 취소 23 | 확인 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/presentation/components/common/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import { icBack } from '@assets/icons'; 4 | import { StBack, StCommonNavigation, StSubmitButton, StTitle } from './style'; 5 | 6 | interface CommonNavigationProps { 7 | isBack?: boolean; 8 | onClickBack?: () => void; 9 | title?: string; 10 | submitButton?: { content: string; onClick: () => void }; 11 | } 12 | 13 | export default function CommonNavigation(props: CommonNavigationProps) { 14 | const { isBack = true, onClickBack = () => navigate(-1), title, submitButton } = props; 15 | const navigate = useNavigate(); 16 | return ( 17 | 18 | {isBack && } 19 | {title && {title}} 20 | {submitButton && ( 21 | {submitButton.content} 22 | )} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaCreateCard/List/index.tsx: -------------------------------------------------------------------------------- 1 | import NeogaCreateCardItem from '../Item'; 2 | import { StNeogaCreateCardList } from './style'; 3 | 4 | interface CardItem { 5 | id: number; 6 | content: string; 7 | isNew: boolean; 8 | src: string; 9 | backgroundColor: string; 10 | title: string; 11 | isCreated: boolean; 12 | } 13 | 14 | interface NeogaCreateCardListProps { 15 | cards: CardItem[]; 16 | onItemClick: (id: number, isCreated: boolean) => void; 17 | } 18 | 19 | function NeogaCreateCardList(props: NeogaCreateCardListProps) { 20 | const { cards, onItemClick } = props; 21 | return ( 22 | 23 | {cards.map((card, idx) => ( 24 | onItemClick(card.id, card.isCreated)} 28 | idx={idx} 29 | /> 30 | ))} 31 | 32 | ); 33 | } 34 | 35 | export default NeogaCreateCardList; 36 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaMainCard/List/index.tsx: -------------------------------------------------------------------------------- 1 | import NeogaMainCardItem from '../Item'; 2 | import { StNeogaMainCardList, StCardWrapper } from './style'; 3 | 4 | interface MainCardItem { 5 | id: number; 6 | title: string; 7 | src: string; 8 | backgroundColor: string; 9 | isCreated: boolean; 10 | } 11 | 12 | interface NeogaMainCardListProps { 13 | cards: MainCardItem[]; 14 | onItemClick: (id: number, isCreated: boolean) => void; 15 | } 16 | 17 | function NeogaMainCardList(props: NeogaMainCardListProps) { 18 | const { cards, onItemClick } = props; 19 | return ( 20 | 21 | 22 | {cards.map((card) => ( 23 | onItemClick(card.id, card.isCreated)} 27 | /> 28 | ))} 29 | 30 | 31 | ); 32 | } 33 | 34 | export default NeogaMainCardList; 35 | -------------------------------------------------------------------------------- /src/presentation/components/NeososeoAnswerCard/Item/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StNeososeoAnswerCard = styled.div` 6 | border-bottom: 1px solid ${COLOR.GRAY_3}; 7 | padding: 25px 0; 8 | 9 | & img { 10 | width: 28px; 11 | height: 28px; 12 | } 13 | & img:nth-child(3) { 14 | width: 52px; 15 | height: 24px; 16 | cursor: pointer; 17 | } 18 | & > div:nth-child(1) { 19 | display: grid; 20 | grid-template-columns: 32px auto 52px; 21 | align-items: center; 22 | gap: 6px; 23 | color: ${COLOR.GRAY_8}; 24 | ${FONT_STYLES.M_15_TITLE}; 25 | } 26 | & > div:nth-child(2) { 27 | margin-top: 8px; 28 | margin-bottom: 16px; 29 | ${FONT_STYLES.R_14_BODY} 30 | color: ${COLOR.GRAY_7}; 31 | word-break: keep-all; 32 | line-height: 20px; 33 | white-space: pre-line; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/presentation/style/common/animation.ts: -------------------------------------------------------------------------------- 1 | import { keyframes } from 'styled-components'; 2 | 3 | export const ANIMATION = { 4 | SWIPE_UP: ({ from }: { from: number }) => keyframes` 5 | from { 6 | transform: translateY(calc(100vh - ${from}px)); 7 | } 8 | to { 9 | transform: translateY(0); 10 | } 11 | `, 12 | SWIPE_DOWN: keyframes` 13 | from { 14 | transform: translateY(0); 15 | } 16 | to { 17 | transform: translateY(100vh); 18 | } 19 | `, 20 | SWIPE_FROM_RIGHT: keyframes` 21 | from { 22 | transform: translateX(100vw); 23 | } 24 | to { 25 | transform: translateX(0); 26 | } 27 | `, 28 | FADE_IN: keyframes` 29 | from { 30 | opacity: 0; 31 | } 32 | to { 33 | opacity: 1; 34 | } 35 | `, 36 | FADE_OUT: keyframes` 37 | from { 38 | opacity: 1; 39 | } 40 | to { 41 | opacity: 0; 42 | } 43 | `, 44 | }; 45 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaMainCard/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import { StNeogaMainCardItem, StTitle } from './style'; 2 | 3 | interface NeogaMainCardItemProps { 4 | id: number; 5 | title: string; 6 | src: string; 7 | backgroundColor: string; 8 | onItemClick: (id: number) => void; 9 | } 10 | 11 | function NeogaMainCardItem(props: NeogaMainCardItemProps) { 12 | const { id, title, src, backgroundColor, onItemClick } = props; 13 | const [firstLine, secondLine] = title.split('\\n'); 14 | const isFirstLineBold = firstLine.includes('*'); 15 | const isSecondLineBold = secondLine.includes('*'); 16 | 17 | return ( 18 | onItemClick(id)}> 19 | 20 | {firstLine.replaceAll('*', '')} 21 | {secondLine.replaceAll('*', '')} 22 | 23 | ); 24 | } 25 | 26 | export default NeogaMainCardItem; 27 | -------------------------------------------------------------------------------- /src/presentation/components/common/ModalWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import { StModalWrapper } from './style'; 4 | 5 | interface ModalWrapperProps { 6 | children: React.ReactNode; 7 | isOpened: boolean; 8 | } 9 | 10 | export default function ModalWrapper(props: ModalWrapperProps) { 11 | const { children, isOpened } = props; 12 | const [isAnimation, setIsAnimation] = useState(true); 13 | 14 | useEffect(() => { 15 | if (isOpened) setTimeout(() => setIsAnimation(false), 300); 16 | else setIsAnimation(true); 17 | }, [isOpened]); 18 | 19 | useEffect(() => { 20 | if (isOpened) document.body.style.overflow = 'hidden'; 21 | else document.body.style.overflow = 'initial'; 22 | return () => { 23 | document.body.style.overflow = 'initial'; 24 | }; 25 | }, [isOpened]); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { QueryClient, QueryClientProvider } from 'react-query'; 4 | 5 | import GlobalStyle from '@styles/global'; 6 | import Router from '@routes/Router'; 7 | import { useLoginUser } from '@hooks/useLoginUser'; 8 | import ToastList from '@components/common/Toast/List'; 9 | 10 | function App() { 11 | const { initLoginUser } = useLoginUser(); 12 | 13 | const queryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | refetchOnWindowFocus: false, 17 | }, 18 | }, 19 | }); 20 | 21 | useEffect(() => { 22 | initLoginUser(); 23 | }, []); 24 | 25 | return ( 26 | <> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | > 35 | ); 36 | } 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/HomeNeoga/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from '@styles/common/font-style'; 4 | import { CORAL_MAIN_BUTTON } from '@styles/common/button'; 5 | 6 | export const StHomeNeogaEmptyView = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | margin-top: 85px; 11 | 12 | div:nth-of-type(1) { 13 | ${FONT_STYLES.SB_18_TITLE}; 14 | color: ${COLOR.GRAY_5}; 15 | font-weight: 600; 16 | margin-bottom: 12px; 17 | } 18 | 19 | div:nth-of-type(2) { 20 | ${FONT_STYLES.M_14_TITLE}; 21 | color: ${COLOR.GRAY_4}; 22 | margin-bottom: 40px; 23 | } 24 | 25 | button { 26 | ${CORAL_MAIN_BUTTON}; 27 | padding: 15px 32px; 28 | margin-bottom: 62px; 29 | border-radius: 14px; 30 | font-size: 15px; 31 | font-weight: 500; 32 | line-height: 21.6px; 33 | letter-spacing: -0.015em; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/presentation/components/common/Keyword/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import styled, { css } from 'styled-components'; 3 | 4 | const linearViewMode = css` 5 | flex-direction: column; 6 | gap: 26px; 7 | padding: 13px 0; 8 | border-top: 1px solid ${COLOR.GRAY_2}; 9 | & > div > div { 10 | width: fit-content; 11 | margin-left: 20px; 12 | & ::after { 13 | position: absolute; 14 | left: 0; 15 | right: 0; 16 | top: 39px; 17 | content: ''; 18 | border-bottom: 1px solid ${COLOR.GRAY_2}; 19 | display: block; 20 | } 21 | } 22 | & > div > div > img:nth-child(2) { 23 | position: absolute; 24 | right: 0; 25 | } 26 | `; 27 | 28 | const flexViewMode = css` 29 | flex-wrap: wrap; 30 | gap: 10px; 31 | `; 32 | 33 | export const StKeywordListLayout = styled.div<{ viewMode?: 'linear' | 'flex' }>` 34 | display: flex; 35 | ${({ viewMode }) => (viewMode === 'linear' ? linearViewMode : flexViewMode)} 36 | `; 37 | -------------------------------------------------------------------------------- /src/infrastructure/api/neoga.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NeogaBannerItem, 3 | NeogaMainCardItem, 4 | NeogaCardItem, 5 | NeogaResultCardItem, 6 | CreateFormInfo, 7 | ResultDetail, 8 | ResultFeedback, 9 | } from './types/neoga'; 10 | 11 | export interface NeogaService { 12 | getBannerTemplate(): Promise; 13 | getMainTemplate(): Promise; 14 | getAllTemplates(viewMode: 'recent' | 'popular'): Promise; 15 | getMainResultCard(): Promise; 16 | getAllFormCard(): Promise; 17 | postAnswerBookmark(answerID: number): Promise<{ isSuccess: boolean }>; 18 | deleteAnswer(answerID: number): Promise<{ isSuccess: boolean }>; 19 | createForm(formID: number): Promise<{ isCreated: boolean; formCode: string }>; 20 | getCreateFormInfo(formID: number): Promise; 21 | getNeososeoInfo(formID: number): Promise; 22 | getNeososeoFeedback(formID: number): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /src/presentation/components/common/Modal/HostDelegation/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { COMMON_MODAL, COMMON_MODAL_BUTTON } from '@styles/common/modal'; 5 | import { FONT_STYLES } from '@styles/common/font-style'; 6 | 7 | export const StHostDelegationModal = styled.div` 8 | ${COMMON_MODAL} 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | padding: 0 24px 24px 24px; 13 | & > *:nth-child(1) { 14 | margin-top: 44px; 15 | color: ${COLOR.GRAY_8}; 16 | ${FONT_STYLES.SB_20_TITLE} 17 | } 18 | & > *:nth-child(2) { 19 | margin-top: 17px; 20 | color: ${COLOR.GRAY_5}; 21 | ${FONT_STYLES.R_15_BODY} 22 | line-height: 143.99%; 23 | white-space: pre-line; 24 | text-align: center; 25 | } 26 | & > :nth-child(3) { 27 | margin-top: 30px; 28 | width: 294px; 29 | } 30 | & > :nth-child(4) { 31 | margin-top: 38px; 32 | ${COMMON_MODAL_BUTTON} 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /src/presentation/components/common/Navigation/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StCommonNavigation = styled.div` 6 | width: 100%; 7 | height: 44px; 8 | position: relative; 9 | & > * { 10 | position: absolute; 11 | } 12 | `; 13 | 14 | export const StBack = styled.img` 15 | margin-left: 14px; 16 | cursor: pointer; 17 | `; 18 | 19 | export const StTitle = styled.div` 20 | top: 14px; 21 | left: 50%; 22 | transform: translate(-50%, 0%); 23 | ${FONT_STYLES.SB_17_TITLE} 24 | line-height: 100%; 25 | letter-spacing: -0.01em; 26 | color: ${COLOR.GRAY_8}; 27 | `; 28 | 29 | export const StSubmitButton = styled.button` 30 | top: 14.33px; 31 | right: 0; 32 | margin-right: 14.33px; 33 | ${FONT_STYLES.M_15_TITLE} 34 | line-height: 100%; 35 | letter-spacing: -0.01em; 36 | color: ${COLOR.CORAL_MAIN}; 37 | background-color: transparent; 38 | `; 39 | -------------------------------------------------------------------------------- /src/presentation/components/common/Modal/TeamLeave/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | import { COMMON_MODAL, COMMON_MODAL_BUTTON } from '@styles/common/modal'; 6 | 7 | export const StWarningMessage = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | gap: 8px; 12 | & > * { 13 | ${FONT_STYLES.R_16_BODY} 14 | color: ${COLOR.GRAY_7}; 15 | } 16 | & > *:first-child { 17 | display: flex; 18 | gap: 3px; 19 | & > *:first-child { 20 | color: ${COLOR.CORAL_MAIN}; 21 | ${FONT_STYLES.SB_16_BODY} 22 | } 23 | } 24 | `; 25 | 26 | export const StDelegationCheckModal = styled.div` 27 | ${COMMON_MODAL} 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | padding: 42px 24px 24px 24px; 32 | & > *:last-child { 33 | margin-top: 27px; 34 | ${COMMON_MODAL_BUTTON} 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/TeamIssue/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StTeamIssueSkeleton, 3 | StTeamIssueTitle, 4 | StTeamIssueCard, 5 | StTeamIssueContent, 6 | StCardFooter, 7 | StImageContainer, 8 | StTeamImage, 9 | StTeamIssueWriter, 10 | StTeamIssueInfo, 11 | } from './style'; 12 | 13 | function TeamIssueSkeleton() { 14 | return ( 15 | 16 | 17 | {new Array(3).fill('').map((_, i) => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ))} 32 | 33 | ); 34 | } 35 | 36 | export default TeamIssueSkeleton; 37 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaResultComment/index.tsx: -------------------------------------------------------------------------------- 1 | import { Keyword } from '@api/types/user'; 2 | import ImmutableKeywordList from '@components/common/Keyword/ImmutableList'; 3 | import { StNeogaResultComment } from './style'; 4 | 5 | interface NeogaResultCommentProps { 6 | name: string; 7 | relationship: string; 8 | content: string; 9 | keyword: Keyword[]; 10 | } 11 | 12 | function NeogaResultComment(props: NeogaResultCommentProps) { 13 | const { name, relationship, content, keyword } = props; 14 | 15 | return ( 16 | 17 | 18 | {name} 19 | · 20 | 너를 {relationship} 21 | 22 | {content} 23 | 24 | { 27 | return; 28 | }} 29 | /> 30 | 31 | 32 | ); 33 | } 34 | 35 | export default NeogaResultComment; 36 | -------------------------------------------------------------------------------- /src/presentation/components/common/HomeHeader/style.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | export const StHomeHeader = styled.div` 6 | & > div { 7 | font-size: 16px; 8 | margin-top: 18px; 9 | } 10 | & > div:nth-child(1) { 11 | margin-top: 0; 12 | } 13 | 14 | & > a { 15 | margin-left: 20px; 16 | } 17 | `; 18 | 19 | export const StNavLink = styled(NavLink)<{ selected: boolean }>` 20 | margin-right: 16px; 21 | padding-bottom: 16px; 22 | color: ${COLOR.GRAY_4}; 23 | cursor: pointer; 24 | 25 | &:nth-of-type(1) { 26 | margin-left: 20px; 27 | } 28 | 29 | ${(props) => 30 | props.selected && 31 | css` 32 | font-weight: 600; 33 | color: ${COLOR.GRAY_8}; 34 | border-bottom: 2px solid ${COLOR.GRAY_8}; 35 | `} 36 | `; 37 | 38 | export const StNavBottomLine = styled.div` 39 | width: 100%; 40 | height: 1px; 41 | background-color: ${COLOR.GRAY_2}; 42 | `; 43 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaFeatures": { 6 | "jsx": true 7 | }, 8 | "ecmaVersion": 2020, 9 | "sourceType": "module" 10 | }, 11 | "env": { 12 | "browser": true, 13 | "node": true 14 | }, 15 | "extends": [ 16 | "eslint:recommended", 17 | "plugin:@typescript-eslint/eslint-recommended", 18 | "plugin:@typescript-eslint/recommended", 19 | "plugin:react/recommended", 20 | "plugin:prettier/recommended" 21 | ], 22 | "settings": { 23 | "react": { 24 | "version": "detect" 25 | } 26 | }, 27 | "plugins": ["react"], 28 | "rules": { 29 | "prettier/prettier": 0, 30 | "react/react-in-jsx-scope": "off", 31 | "react/prop-types": "off", 32 | "react/display-name": "off", 33 | "no-unused-vars": "off", 34 | "@typescript-eslint/no-var-requires": 0, 35 | "@typescript-eslint/no-unused-vars": ["error"], 36 | "@typescript-eslint/explicit-module-boundary-types": "off" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Run ESLint 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the dev branch 8 | pull_request: 9 | branches: [dev] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | env: 15 | CI: true 16 | 17 | jobs: 18 | check-lint: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Set Node.js 16.x 25 | uses: actions/setup-node@v2.4.1 26 | with: 27 | node-version: 16.x 28 | 29 | - name: Install Client Dependencies 30 | run: yarn 31 | 32 | - name: Check Lint Client React App 33 | run: yarn lint 34 | 35 | - uses: actions/upload-artifact@v2 36 | if: ${{ failure() && steps.diff.conclusion == 'failure' }} 37 | with: 38 | name: check_lint 39 | path: check_lint/ 40 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/NeogaDetailForm/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { CORAL_MAIN_BUTTON } from '@styles/common/button'; 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StNeogaDetailFormEmptyView = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | min-height: calc(100vh - 230px); 11 | 12 | & div { 13 | text-align: center; 14 | } 15 | 16 | & div:nth-child(1) { 17 | color: ${COLOR.GRAY_4}; 18 | ${FONT_STYLES.SB_18_TITLE} 19 | } 20 | & div:nth-child(2) { 21 | color: ${COLOR.GRAY_35}; 22 | margin-top: 12px; 23 | ${FONT_STYLES.M_14_TITLE} 24 | } 25 | 26 | button { 27 | align-self: center; 28 | margin-top: 40px; 29 | ${CORAL_MAIN_BUTTON}; 30 | border-radius: 14px; 31 | padding: 15px 50px; 32 | font-weight: 500; 33 | font-size: 15px; 34 | line-height: 143.99%; 35 | letter-spacing: -0.015em; 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /src/presentation/components/common/IssueCardList/index.tsx: -------------------------------------------------------------------------------- 1 | import { TeamMemberNoneId } from '@api/types/team'; 2 | import IssueCard from '../IssueCard'; 3 | 4 | export interface IssueListData { 5 | teamID: string; 6 | issueNumber: number; 7 | issueCardImage?: string; 8 | category: string; 9 | dates: string; 10 | content: string; 11 | issueMembers: TeamMemberNoneId[]; 12 | teamImage?: string; 13 | teamName: string; 14 | memberName: string; 15 | } 16 | 17 | interface IssueListProps { 18 | issueList: IssueListData[]; 19 | onIssueClick: (teamID: string, issueNumber: number) => void; 20 | } 21 | 22 | function IssueCardList(props: IssueListProps) { 23 | const { issueList, onIssueClick } = props; 24 | return ( 25 | 26 | {issueList.map((issue) => ( 27 | onIssueClick(issue.teamID, issue.issueNumber)} 30 | {...issue} 31 | /> 32 | ))} 33 | 34 | ); 35 | } 36 | 37 | export default IssueCardList; 38 | -------------------------------------------------------------------------------- /src/presentation/components/FeedbackCard/ExpandableList/index.tsx: -------------------------------------------------------------------------------- 1 | import { FeedbackDetail } from '@api/types/team'; 2 | import ExpandListButton from '@components/common/ExpandListButton'; 3 | import { useState } from 'react'; 4 | import FeedbackCardList from '../List'; 5 | 6 | type FeedbackCardExpandableListProps = { 7 | feedbacks: FeedbackDetail[]; 8 | firstShown?: number; 9 | }; 10 | 11 | function FeedbackCardExpandableList(props: FeedbackCardExpandableListProps) { 12 | const { feedbacks, firstShown = 2 } = props; 13 | const [isExpanded, setIsExpanded] = useState(false); 14 | return ( 15 | <> 16 | 17 | {firstShown < feedbacks.length && ( 18 | <> 19 | {isExpanded && } 20 | setIsExpanded((prev) => !prev)} 22 | isExpanded={isExpanded} 23 | /> 24 | > 25 | )} 26 | > 27 | ); 28 | } 29 | 30 | export default FeedbackCardExpandableList; 31 | -------------------------------------------------------------------------------- /src/presentation/components/common/Keyword/ImmutableList/index.tsx: -------------------------------------------------------------------------------- 1 | import KeywordItem from '../Item'; 2 | import { StKeywordListLayout } from '../style'; 3 | import { COLOR } from '@styles/common/color'; 4 | import { Keyword } from '@api/types/user'; 5 | 6 | interface ImmutableKeywordListProps { 7 | keywordList: Keyword[]; 8 | viewMode?: 'linear' | 'flex'; 9 | onItemClick: (keyword: Keyword) => void; 10 | isMine?: boolean; 11 | } 12 | 13 | function ImmutableKeywordList(props: ImmutableKeywordListProps) { 14 | const { keywordList, viewMode = 'flex', onItemClick, isMine } = props; 15 | return ( 16 | 17 | {keywordList.map((keyword) => ( 18 | onItemClick(keyword)} 23 | viewMode={viewMode} 24 | isMine={isMine} 25 | /> 26 | ))} 27 | 28 | ); 29 | } 30 | 31 | export default ImmutableKeywordList; 32 | -------------------------------------------------------------------------------- /src/presentation/components/common/Modal/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from '@styles/common/font-style'; 4 | import { COMMON_MODAL, COMMON_MODAL_BUTTON } from '@styles/common/modal'; 5 | 6 | export const StCommonModal = styled.div` 7 | ${COMMON_MODAL} 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | width: 336px; 12 | & > *:nth-child(1) { 13 | margin-top: 32px; 14 | } 15 | & > *:nth-child(2) { 16 | margin-top: 24px; 17 | margin-bottom: 10px; 18 | ${FONT_STYLES.SB_17_BODY} 19 | line-height: 143.99%; 20 | letter-spacing: -0.01em; 21 | color: ${COLOR.GRAY_8}; 22 | } 23 | & > *:last-child { 24 | margin: 34px 19px 24px 19px; 25 | color: ${COMMON_MODAL_BUTTON}; 26 | } 27 | `; 28 | 29 | export const StDescription = styled.div` 30 | margin-top: 10px; 31 | color: ${COLOR.GRAY_5}; 32 | ${FONT_STYLES.R_15_BODY} 33 | line-height: 143.99%; 34 | letter-spacing: -0.01em; 35 | text-align: center; 36 | white-space: pre-line; 37 | `; 38 | -------------------------------------------------------------------------------- /src/assets/images/img_empty_feedback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/infrastructure/mock/login-user.ts: -------------------------------------------------------------------------------- 1 | import { LoginUserService } from '@api/login-user'; 2 | import { LOGIN_USER_DATA } from './login-user.data'; 3 | 4 | export function loginUserMock(): LoginUserService { 5 | const getUserInfo = async () => { 6 | await wait(2000); 7 | return LOGIN_USER_DATA._; 8 | }; 9 | const postLogin = async () => { 10 | await wait(2000); 11 | return { 12 | isJoined: true, 13 | accessToken: '', 14 | refreshToken: '', 15 | user: { 16 | id: 1, 17 | username: '', 18 | userID: '', 19 | profileImage: '', 20 | }, 21 | }; 22 | }; 23 | const postUserInfo = async () => { 24 | await wait(2000); 25 | return { 26 | isJoined: true, 27 | accessToken: '', 28 | refreshToken: '', 29 | user: { 30 | id: 1, 31 | username: '', 32 | userID: '', 33 | profileImage: '', 34 | }, 35 | }; 36 | }; 37 | return { getUserInfo, postLogin, postUserInfo }; 38 | } 39 | 40 | const wait = (milliSeconds: number) => new Promise((resolve) => setTimeout(resolve, milliSeconds)); 41 | -------------------------------------------------------------------------------- /src/presentation/components/common/Keyword/Item/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StKeywordItem = styled.div<{ color: string; fontColor: string }>` 7 | position: relative; 8 | display: flex; 9 | align-items: center; 10 | & > div { 11 | background-color: ${({ color }) => color}; 12 | & > div { 13 | color: ${({ fontColor }) => fontColor}; 14 | } 15 | font-size: 13px; 16 | border-radius: 18px; 17 | padding: 7px 12px; 18 | display: flex; 19 | justify-content: space-between; 20 | gap: 8px; 21 | ${FONT_STYLES.R_13_TITLE} 22 | } 23 | img { 24 | cursor: pointer; 25 | } 26 | `; 27 | 28 | export const StCount = styled.span` 29 | color: ${COLOR.GRAY_4}; 30 | margin-left: 12px; 31 | `; 32 | 33 | export const StMyDeleteBtn = styled.button` 34 | margin-right: 20px; 35 | position: absolute; 36 | right: 0; 37 | color: ${COLOR.GRAY_5}; 38 | ${FONT_STYLES.R_14_TITLE}; 39 | background-color: transparent; 40 | `; 41 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaCreateCard/Item/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StNeogaCreateCardItem = styled.div<{ idx: number }>` 6 | position: relative; 7 | height: 104px; 8 | border-radius: 20px; 9 | padding: 0 22px; 10 | white-space: pre-line; 11 | display: flex; 12 | align-items: center; 13 | cursor: pointer; 14 | box-shadow: 0px 2px 20px rgba(88, 99, 109, 0.05); 15 | z-index: ${({ idx }) => idx}; 16 | margin-bottom: 12px; 17 | 18 | & div { 19 | flex: 1; 20 | & > div:nth-child(1) { 21 | margin-bottom: 9px; 22 | color: ${COLOR.GRAY_8}; 23 | ${FONT_STYLES.SB_16_TITLE} 24 | } 25 | & > div:nth-child(2) { 26 | color: ${COLOR.GRAY_5}; 27 | ${FONT_STYLES.R_14_TITLE} 28 | opacity: 0.8; 29 | } 30 | } 31 | 32 | & img { 33 | width: 52px; 34 | height: 52px; 35 | margin-right: 18px; 36 | } 37 | 38 | & svg { 39 | & path { 40 | stroke: ${COLOR.GRAY_4}; 41 | } 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /src/presentation/pages/Home/Team/Invitation/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { FONT_STYLES } from '@styles/common/font-style'; 5 | 6 | export const StInvitation = styled.div` 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | background-color: ${COLOR.GRAY_1}; 11 | color: ${COLOR.GRAY_6}; 12 | width: 100%; 13 | height: 57px; 14 | ${FONT_STYLES.R_14_TITLE}; 15 | 16 | & div { 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | & > span { 22 | color: ${COLOR.GRAY_5}; 23 | } 24 | 25 | & > div > span { 26 | color: ${COLOR.CORAL_MAIN}; 27 | font-weight: 600; 28 | } 29 | 30 | button { 31 | border-radius: 8px; 32 | padding: 10px 14px; 33 | ${FONT_STYLES.M_13_TITLE}; 34 | } 35 | 36 | button:nth-of-type(1) { 37 | margin-right: 6px; 38 | color: ${COLOR.WHITE}; 39 | background: ${COLOR.CORAL_MAIN}; 40 | } 41 | 42 | button:nth-of-type(2) { 43 | color: ${COLOR.GRAY_6}; 44 | background-color: ${COLOR.GRAY_3}; 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/presentation/components/FeedbackCard/List/index.tsx: -------------------------------------------------------------------------------- 1 | import { FeedbackDetail, FeedbackEditInfo } from '@api/types/team'; 2 | import { MyDetail } from '@api/types/user'; 3 | import FeedbackCardItem from '../Item'; 4 | 5 | type FeedbackCardListProps = { 6 | feedbacks: FeedbackDetail[]; 7 | openBottomSheet?: ( 8 | feedbackID: number, 9 | feedback: FeedbackEditInfo, 10 | isMine: boolean, 11 | isForMe: boolean, 12 | isPinned: boolean, 13 | ) => void; 14 | parentPage?: 'teamsoseo' | 'mypage' | 'myteamsoseo'; 15 | selectedTeam?: MyDetail | null; 16 | }; 17 | 18 | function FeedbackCardList(props: FeedbackCardListProps) { 19 | const { feedbacks, openBottomSheet, parentPage = 'mypage', selectedTeam } = props; 20 | 21 | return ( 22 | <> 23 | {feedbacks.map((feedback) => ( 24 | 31 | ))} 32 | > 33 | ); 34 | } 35 | 36 | export default FeedbackCardList; 37 | -------------------------------------------------------------------------------- /src/presentation/components/NeososeoAnswerCard/ExpandableList/index.tsx: -------------------------------------------------------------------------------- 1 | import { AnswerDetail } from '@api/types/user'; 2 | import ExpandListButton from '@components/common/ExpandListButton'; 3 | import { useState } from 'react'; 4 | import NeososeoAnswerCardList from '../List'; 5 | 6 | type NeososeoAnswerCardExpandableListProps = { 7 | answers: AnswerDetail[]; 8 | firstShown?: number; 9 | }; 10 | 11 | function NeososeoAnswerCardExpandableList(props: NeososeoAnswerCardExpandableListProps) { 12 | const { answers, firstShown = 2 } = props; 13 | const [isExpanded, setIsExpanded] = useState(false); 14 | 15 | return ( 16 | <> 17 | 18 | {firstShown < answers.length && ( 19 | <> 20 | {isExpanded && } 21 | setIsExpanded((prev) => !prev)} 23 | isExpanded={isExpanded} 24 | /> 25 | > 26 | )} 27 | > 28 | ); 29 | } 30 | 31 | export default NeososeoAnswerCardExpandableList; 32 | -------------------------------------------------------------------------------- /src/assets/icons/ic_email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/ic_empty_feedback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/icons/ic_link_copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaMainCard/Item/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StNeogaMainCardItem = styled.div` 6 | cursor: pointer; 7 | background-color: ${(props) => props.color}; 8 | ${FONT_STYLES.M_15_TITLE}; 9 | border-radius: 18.1233px; 10 | color: ${COLOR.WHITE}; 11 | width: 136px; 12 | padding: 27px 13px 21px 13px; 13 | word-break: keep-all; 14 | 15 | & + & { 16 | margin-left: 8px; 17 | } 18 | 19 | & img { 20 | width: 44px; 21 | height: 44px; 22 | object-fit: cover; 23 | margin-bottom: 20px; 24 | } 25 | 26 | & div + div { 27 | margin-top: 2px; 28 | } 29 | `; 30 | 31 | export const StTitle = styled.div<{ isBold: boolean }>` 32 | font-weight: ${(props) => (props.isBold ? 600 : 400)}; 33 | font-size: ${(props) => (props.isBold ? '16px' : '14px')}; 34 | letter-spacing: ${(props) => (props.isBold ? '-0.015em' : '-0.01em')}; 35 | color: ${(props) => (props.isBold ? COLOR.GRAY_1 : COLOR.WHITE)}; 36 | line-height: 140%; 37 | padding-left: 2px; 38 | `; 39 | -------------------------------------------------------------------------------- /src/presentation/components/common/Empty/MyPage/style.ts: -------------------------------------------------------------------------------- 1 | import { CORAL_MAIN_BUTTON } from '@styles/common/button'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from '@styles/common/font-style'; 4 | import styled from 'styled-components'; 5 | 6 | export const StMyEmptyView = styled.div` 7 | height: 390px; 8 | justify-content: center; 9 | display: flex; 10 | align-items: center; 11 | flex-direction: column; 12 | 13 | & > div:nth-child(1) { 14 | ${FONT_STYLES.SB_18_TITLE} 15 | color: ${COLOR.GRAY_4}; 16 | } 17 | & > div:nth-child(2) { 18 | margin-top: 14px; 19 | ${FONT_STYLES.R_14_BODY} 20 | color: ${COLOR.GRAY_4}; 21 | } 22 | `; 23 | 24 | export const StLabel = styled.div` 25 | margin-bottom: 15px; 26 | text-align: center; 27 | color: ${COLOR.GRAY_5}; 28 | ${FONT_STYLES.R_15_TITLE} 29 | line-height: 1.4em; 30 | `; 31 | 32 | export const StButton = styled.div` 33 | ${CORAL_MAIN_BUTTON} 34 | padding: 15px 16px; 35 | border-radius: 14px; 36 | font-size: 15px; 37 | line-height: 1.44em; 38 | letter-spacing: -0.0015em; 39 | margin-top: 15px; 40 | cursor: pointer; 41 | `; 42 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/TSSPick/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StImage, 3 | StName, 4 | StTeamContainer, 5 | StTitle, 6 | StHeader, 7 | StTssTitle, 8 | StButton, 9 | StBody, 10 | StKeywordContainer, 11 | StKeyword, 12 | StContent, 13 | } from './style'; 14 | 15 | function TSSPickSkeleton() { 16 | return ( 17 | <> 18 | 19 | 20 | {new Array(2).fill('').map((_, i) => ( 21 | 22 | 23 | 24 | 25 | ))} 26 | 27 | {new Array(2).fill('').map((_, i) => ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ))} 43 | > 44 | ); 45 | } 46 | 47 | export default TSSPickSkeleton; 48 | -------------------------------------------------------------------------------- /src/assets/icons/ic_setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/application/utils/browser.ts: -------------------------------------------------------------------------------- 1 | const getBrowser = () => { 2 | const agent = navigator.userAgent.toLowerCase(); 3 | if (agent.indexOf('chrome') !== -1) return 'Chrome'; 4 | if (agent.indexOf('opera') !== -1) return 'Opera'; 5 | if (agent.indexOf('staroffice') !== -1) return 'Star Office'; 6 | if (agent.indexOf('webtv') !== -1) return 'WebTV'; 7 | if (agent.indexOf('beonex') !== -1) return 'Beonex'; 8 | if (agent.indexOf('chimera') !== -1) return 'Chimera'; 9 | if (agent.indexOf('netpositive') !== -1) return 'NetPositive'; 10 | if (agent.indexOf('phoenix') !== -1) return 'Phoenix'; 11 | if (agent.indexOf('firefox') !== -1) return 'Firefox'; 12 | if (agent.indexOf('safari') !== -1) return 'Safari'; 13 | if (agent.indexOf('skipstone') !== -1) return 'SkipStone'; 14 | if (agent.indexOf('netscape') !== -1) return 'Netscape'; 15 | if (agent.indexOf('mozilla/5.0') !== -1) return 'Mozilla'; 16 | if (agent.indexOf('msie') !== -1) return 'Internet Explorer'; 17 | else return 'unknown browser'; 18 | }; 19 | 20 | export const checkBrowser = (...args: string[]) => { 21 | const currentBrowser = getBrowser(); 22 | return args.includes(currentBrowser); 23 | }; 24 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/TemplateList/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import StSkeletonItem from '../style'; 4 | import { COLOR } from '@styles/common/color'; 5 | 6 | export const StTemplateListSkeleton = styled.div` 7 | margin-top: 52px; 8 | display: flex; 9 | flex-direction: column; 10 | padding: 0 20px; 11 | width: 100%; 12 | `; 13 | 14 | export const StText = styled(StSkeletonItem)` 15 | height: 16px; 16 | background-color: ${COLOR.GRAY_3}; 17 | `; 18 | 19 | export const StShortText = styled(StText)` 20 | width: 164px; 21 | margin-bottom: 8px; 22 | `; 23 | 24 | export const StLongText = styled(StText)` 25 | width: 219px; 26 | margin-bottom: 25px; 27 | `; 28 | 29 | export const StCardContainer = styled.div` 30 | display: flex; 31 | flex-wrap: nowrap; 32 | width: max-content; 33 | gap: 8px; 34 | padding-left: 20px; 35 | margin-left: -20px; 36 | margin-bottom: 41px; 37 | `; 38 | 39 | export const StCard = styled(StSkeletonItem)` 40 | box-sizing: border-box; 41 | width: 136px; 42 | height: 156px; 43 | border-radius: 17px; 44 | background-color: ${COLOR.GRAY_2}; 45 | overflow: hidden; 46 | `; 47 | -------------------------------------------------------------------------------- /src/presentation/components/common/ImageUpload/style.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const StThumbnail = styled.img<{ styles: React.CSSProperties }>` 5 | object-fit: cover; 6 | width: ${(props) => props.styles.width}; 7 | height: ${(props) => props.styles.height}; 8 | border-radius: ${(props) => props.styles.borderRadius ?? 0}; 9 | `; 10 | 11 | export const StImageUpload = styled.div<{ styles: React.CSSProperties }>` 12 | width: ${(props) => props.styles.width}; 13 | cursor: pointer; 14 | position: relative; 15 | `; 16 | 17 | export const StThumbnailWrapper = styled.div<{ styles: React.CSSProperties }>` 18 | width: ${(props) => props.styles.width}; 19 | height: ${(props) => props.styles.height}; 20 | border-radius: ${(props) => props.styles.borderRadius ?? 0}; 21 | `; 22 | 23 | export const StDefaultChildren = styled.img<{ styles: React.CSSProperties }>` 24 | bottom: ${(props) => props.styles.bottom ?? 0}; 25 | right: ${(props) => props.styles.right ?? 0}; 26 | width: ${(props) => props.styles.width}; 27 | height: ${(props) => props.styles.height ?? props.styles.width}; 28 | position: absolute; 29 | `; 30 | -------------------------------------------------------------------------------- /src/presentation/pages/OAuthRedirectHandler/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useMutation } from 'react-query'; 4 | 5 | import { useLoginUser } from '@hooks/useLoginUser'; 6 | import { api } from '@api/index'; 7 | import { UnauthorizedError } from '@api/types/errors'; 8 | 9 | const OAuthRedirectHandler = () => { 10 | const navigate = useNavigate(); 11 | const { saveLoginUser } = useLoginUser(); 12 | 13 | const { mutate: login } = useMutation( 14 | (authorizationCode: string) => api.loginUserService.postLogin(authorizationCode), 15 | { 16 | useErrorBoundary: true, 17 | onSuccess: (data) => { 18 | saveLoginUser(data); 19 | if (data.isJoined) navigate('/home'); 20 | else navigate('/join'); 21 | }, 22 | }, 23 | ); 24 | 25 | useEffect(() => { 26 | const authorizationCode = new URL(window.location.href).searchParams.get('code') ?? ''; 27 | if (authorizationCode.length) login(authorizationCode); 28 | else throw new UnauthorizedError('카카오 인가 코드 조회 실패'); 29 | }, []); 30 | 31 | return <>>; 32 | }; 33 | 34 | export default OAuthRedirectHandler; 35 | -------------------------------------------------------------------------------- /src/presentation/components/common/SelectBox/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import styled from 'styled-components'; 3 | 4 | export const StSelectBoxWrapper = styled.div` 5 | position: relative; 6 | cursor: pointer; 7 | `; 8 | 9 | export const StSelectBoxHeader = styled.div` 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | padding: 0 16px; 14 | height: 52px; 15 | border-radius: 16px; 16 | color: ${COLOR.GRAY_6}; 17 | border: 1px solid ${COLOR.GRAY_3}; 18 | z-index: 1; 19 | `; 20 | 21 | export const StSelectBoxItem = styled.div<{ selected: boolean }>` 22 | height: 52px; 23 | padding: 0 16px; 24 | display: flex; 25 | align-items: center; 26 | color: ${({ selected }) => (selected ? COLOR.GRAY_6 : COLOR.GRAY_5)}; 27 | background-color: white; 28 | `; 29 | 30 | export const StSelectBoxTail = styled.div` 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | width: 100%; 35 | padding-top: 52px; 36 | border-radius: 16px; 37 | border: 2px solid ${COLOR.CORAL_MAIN}; 38 | z-index: 2; 39 | & div:last-child { 40 | border-bottom-left-radius: 16px; 41 | border-bottom-right-radius: 16px; 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /src/presentation/pages/Team/Main/MemberPopup/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import { TeamMemberNoneId } from '@api/types/team'; 4 | import { MAX_TEAM_MEMBER } from '@utils/constant'; 5 | import { StTeamMemberPopup, StWholeButton } from './style'; 6 | import { icWhole } from '@assets/icons'; 7 | import { imgEmptyProfile } from '@assets/images'; 8 | 9 | interface TeamMemberPopupProps { 10 | members: TeamMemberNoneId[]; 11 | teamID: number; 12 | } 13 | 14 | function TeamMemberPopup(props: TeamMemberPopupProps) { 15 | const { members, teamID } = props; 16 | const slicedMemberList = members.slice(0, MAX_TEAM_MEMBER); 17 | const navigate = useNavigate(); 18 | 19 | return ( 20 | 21 | {slicedMemberList.map(({ id, profileName, profileImage }) => ( 22 | 23 | 24 | {profileName} 25 | 26 | ))} 27 | navigate(`/team/${teamID}/member`)}> 28 | 전체보기 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export default TeamMemberPopup; 36 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaFormImageToSave/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import NeogaFormTicket from '@components/NeogaFormTicket'; 4 | import { StLogo, StNeogaFormImageToSave } from './style'; 5 | import { NeososeoFormData } from '@api/types/neososeo-form'; 6 | import { imgCharacterLogo, imgEmptyProfileSmall } from '@assets/images'; 7 | 8 | interface NeogaFormImageToSaveProps { 9 | formData: NeososeoFormData; 10 | } 11 | 12 | export const NeogaFormImageToSave = React.forwardRef( 13 | (props, ref) => { 14 | const { title, content, imageSub, userName, createdAt, userProfileImage } = props.formData; 15 | return ( 16 | 17 | 18 | 19 | 20 | {`${userName}님의 너가소개서`} 21 | {createdAt} 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/assets/icons/ic_crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/infrastructure/remote/service-report.ts: -------------------------------------------------------------------------------- 1 | import { ReportService } from '@api/service-report'; 2 | import { privateAPI } from './base'; 3 | 4 | export function reportRemote(): ReportService { 5 | const getServiceCenterCategories = async () => { 6 | const response = await privateAPI.get({ url: '/report' }); 7 | return response.data.reportCategory.map((category: any) => ({ 8 | id: category.id, 9 | content: category.name, 10 | })); 11 | }; 12 | 13 | const postReport = async ( 14 | categoryID: number, 15 | title: string, 16 | content: string, 17 | email: string, 18 | image?: File, 19 | ) => { 20 | const formData = new FormData(); 21 | formData.append('reportCategoryId', categoryID.toString()); 22 | formData.append('title', title); 23 | formData.append('content', content); 24 | formData.append('email', email); 25 | image && formData.append('image', image); 26 | const response = await privateAPI 27 | .post({ url: '/report', data: formData, type: 'multipart' }) 28 | .catch((error) => { 29 | throw error; 30 | }); 31 | return { isSuccess: response.success }; 32 | }; 33 | 34 | return { getServiceCenterCategories, postReport }; 35 | } 36 | -------------------------------------------------------------------------------- /src/presentation/routes/FormRouter.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router-dom'; 2 | import { lazy } from 'react'; 3 | const NeososeoFormAnswer = lazy(() => import('@pages/NeososeoForm/Answer')); 4 | const NeososeoFormHome = lazy(() => import('@pages/NeososeoForm/Home')); 5 | const NeososeoFormIntro = lazy(() => import('@pages/NeososeoForm/Intro')); 6 | import TeamIssueKeyword from '@pages/Team/Issue/Keyword'; 7 | import PublicRoute from './common/PublicRoute'; 8 | import NeososeoFormPage from '@pages/NeososeoForm'; 9 | import NeososeoFormFinish from '@pages/NeososeoForm/Finish'; 10 | 11 | function FormRouter() { 12 | return ( 13 | 14 | }> 15 | }> 16 | } /> 17 | } /> 18 | }> 19 | } /> 20 | 21 | 22 | } /> 23 | 24 | 25 | ); 26 | } 27 | 28 | export default FormRouter; 29 | -------------------------------------------------------------------------------- /src/presentation/pages/Neoga/Create/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StNeogaCreate = styled.div` 6 | width: 100%; 7 | min-height: 100vh; 8 | `; 9 | 10 | const StWrapper = styled.div` 11 | padding: 20px; 12 | padding-top: 0; 13 | `; 14 | 15 | export const StWhiteWrapper = styled(StWrapper)` 16 | & > a > img { 17 | width: 44px; 18 | height: 44px; 19 | } 20 | & > div:nth-child(1) { 21 | ${FONT_STYLES.SB_24_TITLE}; 22 | margin-top: 12px; 23 | } 24 | & > div:nth-child(2) { 25 | ${FONT_STYLES.R_15_TITLE}; 26 | color: ${COLOR.GRAY_6}; 27 | margin-top: 12px; 28 | margin-bottom: 32px; 29 | } 30 | & > div:nth-child(3) { 31 | display: flex; 32 | gap: 8px; 33 | margin-bottom: 20px; 34 | } 35 | `; 36 | 37 | export const StViewModeSelector = styled.div<{ selected: boolean }>` 38 | cursor: pointer; 39 | border-radius: 12px; 40 | padding: 8px 20px; 41 | color: ${({ selected }) => (selected ? COLOR.WHITE : COLOR.GRAY_5)}; 42 | background-color: ${({ selected }) => (selected ? COLOR.GRAY_7 : COLOR.GRAY_1)}; 43 | ${FONT_STYLES.M_13_TITLE} 44 | `; 45 | -------------------------------------------------------------------------------- /src/assets/icons/ic_link_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaFormImageToSave/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | 5 | export const StNeogaFormImageToSave = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | width: 100%; 10 | height: 696px; 11 | padding-top: 156px; 12 | background-color: ${COLOR.GRAY_1}; 13 | & > *:first-child { 14 | width: 100%; 15 | display: flex; 16 | justify-content: center; 17 | gap: 12px; 18 | margin-bottom: 22px; 19 | & > *:first-child { 20 | width: 48px; 21 | height: 48px; 22 | border-radius: 16.0349px; 23 | object-fit: cover; 24 | } 25 | & > *:last-child { 26 | padding-top: 9px; 27 | & > *:first-child { 28 | font-weight: 600; 29 | font-size: 16px; 30 | color: ${COLOR.GRAY_8}; 31 | margin-bottom: 6px; 32 | } 33 | & > *:last-child { 34 | font-weight: 400; 35 | font-size: 11px; 36 | color: ${COLOR.GRAY_5}; 37 | } 38 | } 39 | } 40 | `; 41 | 42 | export const StLogo = styled.img` 43 | position: absolute; 44 | bottom: 22px; 45 | height: 22px; 46 | width: 92px; 47 | object-fit: cover; 48 | `; 49 | -------------------------------------------------------------------------------- /src/assets/icons/ic_link_coral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/application/hooks/useScrollHeight.ts: -------------------------------------------------------------------------------- 1 | import { SCROLL_BOTTOM_PADDING } from '@utils/constant'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | 4 | export function useScrollHeight() { 5 | const [isInitialState, setIsInitialState] = useState(true); 6 | const [isBottomReached, setIsBottomReached] = useState([]); 7 | const handleScroll = useCallback(() => { 8 | const scrollHeight = Math.max( 9 | document.body.scrollHeight, 10 | document.documentElement.scrollHeight, 11 | document.body.offsetHeight, 12 | document.documentElement.offsetHeight, 13 | document.body.clientHeight, 14 | document.documentElement.clientHeight, 15 | ); 16 | const scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop); 17 | const documentHeight = document.documentElement.clientHeight; 18 | if (scrollTop + documentHeight >= scrollHeight - SCROLL_BOTTOM_PADDING) { 19 | setIsBottomReached([]); 20 | setIsInitialState(false); 21 | } 22 | }, []); 23 | 24 | useEffect(() => { 25 | document.addEventListener('scroll', handleScroll); 26 | return () => document.removeEventListener('scroll', handleScroll); 27 | }, []); 28 | 29 | return { isBottomReached, isInitialState }; 30 | } 31 | -------------------------------------------------------------------------------- /src/presentation/pages/Team/Edit/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StTeamEdit = styled.div` 6 | padding: 0 20px; 7 | width: 100%; 8 | position: relative; 9 | & > *:nth-child(1) { 10 | margin-top: 23px; 11 | font-weight: 600; 12 | font-size: 24px; 13 | line-height: 100%; 14 | letter-spacing: -0.01em; 15 | color: ${COLOR.GRAY_8}; 16 | } 17 | & > *:nth-child(2) { 18 | margin-top: 31px; 19 | width: 88px; 20 | } 21 | & > *:nth-child(7) { 22 | margin-top: 48px; 23 | width: 100%; 24 | height: 46px; 25 | ${FONT_STYLES.M_14_TITLE} 26 | line-height: 100%; 27 | letter-spacing: -0.01em; 28 | color: ${COLOR.GRAY_5}; 29 | border: 1px solid #e9e9e9; 30 | border-radius: 16px; 31 | background-color: transparent; 32 | } 33 | & > *:nth-child(8) { 34 | margin-top: 14px; 35 | margin-bottom: 177px; 36 | width: 100%; 37 | ${FONT_STYLES.R_12_TITLE} 38 | line-height: 100%; 39 | letter-spacing: -0.01em; 40 | color: ${COLOR.GRAY_4}; 41 | } 42 | `; 43 | 44 | export const StRelativeWrapper = styled.div` 45 | position: relative; 46 | `; 47 | -------------------------------------------------------------------------------- /src/assets/icons/ic_copy_mypage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/icons/ic_link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/presentation/components/MyItem/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from '@styles/common/font-style'; 4 | 5 | export const StMyItem = styled.button<{ 6 | isSquare: boolean; 7 | isSelected: boolean | undefined; 8 | img: string; 9 | }>` 10 | width: ${({ isSquare }) => (isSquare ? '42px' : '43px')}; 11 | background-color: transparent; 12 | -webkit-tap-highlight-color: transparent; 13 | 14 | div { 15 | background: no-repeat center/cover url(${(props) => props.img}); 16 | width: 100%; 17 | height: ${({ isSquare }) => (isSquare ? '42px' : '43px')}; 18 | border: ${(props) => (props.isSelected ? (props.isSquare ? '1.5px' : '1px') : '0px')} solid 19 | ${COLOR.CORAL_MAIN}; 20 | border-radius: ${({ isSquare }) => (isSquare ? '16px' : '50%')}; 21 | opacity: ${({ isSelected }) => (isSelected ? 1 : 0.5)}; 22 | object-fit: cover; 23 | } 24 | 25 | span { 26 | display: block; 27 | margin-top: 8px; 28 | ${FONT_STYLES.R_13_TITLE}; 29 | color: ${({ isSelected }) => (isSelected ? COLOR.GRAY_8 : COLOR.GRAY_4)}; 30 | text-overflow: ellipsis; 31 | white-space: nowrap; 32 | overflow: hidden; 33 | } 34 | 35 | &:not(:last-child) { 36 | margin-right: 14px; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /src/infrastructure/api/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EditProfileInfo, 3 | Keyword, 4 | MyKeywordInfo, 5 | MyPageInfo, 6 | NeososeoAnswerBookmark, 7 | TeamFeedbackBookmark, 8 | MyAnswerInfo, 9 | MyFeedbackInfo, 10 | MyFormInfo, 11 | MyTeamInfo, 12 | } from './types/user'; 13 | 14 | export interface UserService { 15 | getKeywords(userID: number, page: number): Promise; 16 | postKeyword(userID: number, content: string): Promise; 17 | undoPostKeyword(keywordID: string): Promise<{ isSuccess: boolean }>; 18 | getMyPageInfo(userID: string): Promise; 19 | getNeososeoBookmark(userID: string): Promise; 20 | getFeedbackBookmark(userID: string): Promise; 21 | getDuplicationCheck(userID: string): Promise<{ isDuplicate: boolean }>; 22 | editUserProfile(formData: FormData): Promise; 23 | getMyKeywordList(page: number): Promise; 24 | deleteMyKeyword(keywordID: number): Promise<{ isSuccess: boolean }>; 25 | getMyFormInfo(): Promise; 26 | getMyAnswerInfo(page: number, formID?: number): Promise; 27 | getMyTeamInfo(): Promise; 28 | getMyFeedbackInfo(page: number, teamID?: number): Promise; 29 | postWithdraw(): Promise<{ isSuccess: boolean }>; 30 | } 31 | -------------------------------------------------------------------------------- /src/presentation/components/common/Modal/DeleteFeedback/index.tsx: -------------------------------------------------------------------------------- 1 | import { api } from '@api/index'; 2 | import { useMutation, useQueryClient } from 'react-query'; 3 | import { useParams } from 'react-router'; 4 | import CommonModal from '..'; 5 | 6 | type DeleteFeedbackModalProps = { 7 | isOpened: boolean; 8 | closeModal(): void; 9 | closeBottomSheet(): void; 10 | feedbackID: number; 11 | }; 12 | 13 | function DeleteFeedbackModal(props: DeleteFeedbackModalProps) { 14 | const { closeModal, closeBottomSheet, feedbackID, isOpened } = props; 15 | const { teamID, issueID } = useParams(); 16 | const queryClient = useQueryClient(); 17 | const deleteFeedback = async () => { 18 | const response = await api.teamService.deleteFeedback(feedbackID); 19 | return response.isSuccess; 20 | }; 21 | const { mutate: mutateDeleteFeedback } = useMutation(deleteFeedback, { 22 | onSuccess: () => { 23 | closeModal(); 24 | closeBottomSheet(); 25 | return queryClient.invalidateQueries(['issueDetailData', `${teamID}-${issueID}`]); 26 | }, 27 | }); 28 | return ( 29 | 35 | ); 36 | } 37 | 38 | export default DeleteFeedbackModal; 39 | -------------------------------------------------------------------------------- /src/presentation/pages/Login/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from '@styles/common/font-style'; 4 | 5 | export const StLogin = styled.div` 6 | padding: 0 20px; 7 | position: relative; 8 | 9 | & > img:first-child { 10 | margin-top: 50px; 11 | width: 59px; 12 | height: 59px; 13 | } 14 | `; 15 | 16 | export const StTitle = styled.div` 17 | font-weight: 600; 18 | ${FONT_STYLES.SB_24_TITLE}; 19 | color: ${COLOR.GRAY_8}; 20 | margin-top: 12px; 21 | `; 22 | 23 | export const StNoticeWrapper = styled.div` 24 | display: flex; 25 | ${FONT_STYLES.R_18_BODY}; 26 | line-height: 144%; 27 | margin-top: 24px; 28 | color: ${COLOR.GRAY_5}; 29 | `; 30 | 31 | export const StLoginButton = styled.button` 32 | display: flex; 33 | align-items: center; 34 | padding: 18px 28px; 35 | width: min(350px, calc(100% - 40px)); 36 | height: 58px; 37 | background-color: #fee500; 38 | border-radius: 18px; 39 | position: fixed; 40 | bottom: 88px; 41 | 42 | & > span { 43 | ${FONT_STYLES.M_16_TITLE}; 44 | color: ${COLOR.GRAY_8}; 45 | flex: 1; 46 | } 47 | `; 48 | 49 | export const StLoginImg = styled.img` 50 | position: fixed; 51 | top: 50%; 52 | left: 50%; 53 | transform: translate(-50%, -50%); 54 | `; 55 | -------------------------------------------------------------------------------- /src/presentation/components/ProfileListSelectable/index.tsx: -------------------------------------------------------------------------------- 1 | import ProfileItem from '@components/common/ProfileItem'; 2 | import { StItemWrapper, StProfileList } from '@components/common/ProfileList/style'; 3 | 4 | interface ProfileListData { 5 | id: number; 6 | profileImage?: string; 7 | profileName: string; 8 | } 9 | 10 | interface ProfileListProps { 11 | isSquare: boolean; 12 | profiles: ProfileListData[]; 13 | selectedProfile: ProfileListData | null; 14 | setSelectedProfile: (profile: ProfileListData) => void; 15 | } 16 | 17 | function ProfileListSelectable(props: ProfileListProps) { 18 | const { isSquare, profiles, selectedProfile, setSelectedProfile } = props; 19 | 20 | return ( 21 | 22 | 23 | {profiles.map(({ id, profileImage, profileName }) => ( 24 | setSelectedProfile({ id, profileImage, profileName })} 32 | /> 33 | ))} 34 | 35 | 36 | ); 37 | } 38 | 39 | export default ProfileListSelectable; 40 | -------------------------------------------------------------------------------- /src/presentation/pages/Team/Register/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { COMMON_INPUT } from '@styles/common/input'; 3 | import { FULL_WIDTH_BUTTON } from '@styles/common/button'; 4 | import { COLOR } from '@styles/common/color'; 5 | 6 | export const StWrapper = styled.div` 7 | width: 100%; 8 | min-height: 100vh; 9 | position: relative; 10 | & > * { 11 | position: absolute; 12 | } 13 | `; 14 | 15 | export const StTeamRegister = styled.div` 16 | width: 100%; 17 | height: 100%; 18 | & > *:last-child { 19 | width: 100%; 20 | padding: 0 20px; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: flex-start; 24 | } 25 | `; 26 | 27 | export const StTitle = styled.div` 28 | font-weight: 600; 29 | font-size: 24px; 30 | color: ${COLOR.GRAY_8}; 31 | margin-top: 24px; 32 | margin-bottom: 30px; 33 | `; 34 | 35 | export const StTextarea = styled.textarea` 36 | ${COMMON_INPUT} 37 | margin-top: 18px; 38 | width: 100%; 39 | min-height: 100px; 40 | resize: none; 41 | `; 42 | 43 | export const StSubmitButton = styled.button<{ isActive: boolean }>` 44 | background-color: ${(props) => (props.isActive ? COLOR.CORAL_MAIN : COLOR.GRAY_3)}; 45 | color: ${COLOR.WHITE}; 46 | ${FULL_WIDTH_BUTTON} 47 | margin-top: 44px; 48 | margin-bottom: 48px; 49 | `; 50 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaDetailFormCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { FeedAnswer } from '@api/types/neoga'; 2 | import { IcMeatball } from '@assets/icons'; 3 | import ImmutableKeywordList from '@components/common/Keyword/ImmutableList'; 4 | import { 5 | StFeedContent, 6 | StFeedMore, 7 | StFeedHeader, 8 | StFeedName, 9 | StNeogaDetailFormCard, 10 | } from './style'; 11 | 12 | type NeogaDetailFormCardProps = FeedAnswer & { 13 | openBottomSheet: (isPinned: boolean, id: number) => void; 14 | }; 15 | 16 | function NeogaDetailFormCard(props: NeogaDetailFormCardProps) { 17 | const { id, name, relationship, content, createdAt, keywordList, openBottomSheet, isPinned } = 18 | props; 19 | 20 | return ( 21 | 22 | 23 | 24 | {name} 25 | 너를 {relationship} 26 | · 27 | {createdAt} 28 | 29 | 30 | openBottomSheet(isPinned, id)} /> 31 | 32 | 33 | {content} 34 | null} /> 35 | 36 | ); 37 | } 38 | 39 | export default NeogaDetailFormCard; 40 | -------------------------------------------------------------------------------- /src/presentation/components/UserSearchResult/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SearchedUserForEdit, SearchedUserForRegister } from '@api/types/team'; 4 | import UserSearchEmptyView from '@components/common/Empty/UserSearch'; 5 | import CommonLoader from '@components/common/Loader'; 6 | import SearchedUserItem from '@components/SearchedUserItem'; 7 | import { StUserSearchResultForTeamRegister } from './style'; 8 | 9 | interface UserSearchResultProps { 10 | isFetchingNextPage: boolean; 11 | searchedUserList: SearchedUserForRegister[] | SearchedUserForEdit[] | null; 12 | } 13 | 14 | export default function UserSearchResult(props: UserSearchResultProps) { 15 | const { isFetchingNextPage, searchedUserList } = props; 16 | 17 | return ( 18 | 19 | 검색 결과 20 | {searchedUserList === null ? ( 21 | <>> 22 | ) : searchedUserList.length ? ( 23 | searchedUserList.map((user) => { 24 | return ( 25 | 26 | 27 | {isFetchingNextPage && } 28 | 29 | ); 30 | }) 31 | ) : ( 32 | 33 | )} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/presentation/components/common/Keyword/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import { Keyword } from '@api/types/user'; 2 | import { icCloseGrey, icCloseWhite } from '@assets/icons'; 3 | import { COLOR } from '@styles/common/color'; 4 | import { StKeywordItem, StCount, StMyDeleteBtn } from './style'; 5 | 6 | interface Props extends Keyword { 7 | isMutable: boolean; 8 | isMine?: boolean; 9 | onDeleteClick?: () => void; 10 | onItemClick?: () => void; 11 | viewMode: 'linear' | 'flex'; 12 | } 13 | 14 | function KeywordItem(props: Props) { 15 | const { 16 | isMutable, 17 | isMine, 18 | content, 19 | color, 20 | fontColor, 21 | onDeleteClick, 22 | onItemClick, 23 | viewMode, 24 | count, 25 | } = props; 26 | return ( 27 | 28 | 29 | {content} 30 | {isMutable && !isMine && ( 31 | 35 | )} 36 | 37 | {viewMode === 'linear' && {count}} 38 | {isMine && isMutable && 삭제} 39 | 40 | ); 41 | } 42 | 43 | export default KeywordItem; 44 | -------------------------------------------------------------------------------- /src/presentation/components/common/Skeleton/CardItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StTitle, 3 | StSubtitle, 4 | StCardItemSkeleton, 5 | StCard, 6 | StCardHeader, 7 | StImage, 8 | StNssTitle, 9 | StNssContent, 10 | StLongText, 11 | StKeywordContainer, 12 | StKeyword, 13 | } from './style'; 14 | 15 | function CardItemSkeleton() { 16 | return ( 17 | 18 | 19 | 20 | {new Array(2).fill('').map((_, i) => ( 21 | 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 | ))} 47 | 48 | ); 49 | } 50 | 51 | export default CardItemSkeleton; 52 | -------------------------------------------------------------------------------- /src/application/hooks/useImageUpload.ts: -------------------------------------------------------------------------------- 1 | import { icEdit, icTrash } from '@assets/icons'; 2 | import { useRef, useState } from 'react'; 3 | 4 | export default function useImageUpload() { 5 | const [image, setImage] = useState(); 6 | const fileInputRef = useRef(null); 7 | const [bottomSheetOpened, setBottomSheetOpened] = useState(false); 8 | 9 | const clickFileInputRef = () => fileInputRef.current && fileInputRef.current.click(); 10 | 11 | const removeImage = () => { 12 | setImage(null); 13 | setBottomSheetOpened(false); 14 | }; 15 | 16 | const openBottomSheet = () => setBottomSheetOpened(true); 17 | const closeBottomSheet = () => setBottomSheetOpened(false); 18 | const bottomSheetButtonList = [ 19 | { 20 | icon: icEdit, 21 | label: '수정하기', 22 | onClick: clickFileInputRef, 23 | }, 24 | { icon: icTrash, label: '기본 이미지로 변경', onClick: removeImage }, 25 | ]; 26 | 27 | const imageUploadProps = { 28 | ref: fileInputRef, 29 | openBottomSheet: openBottomSheet, 30 | closeBottomSheet: closeBottomSheet, 31 | onClickInput: clickFileInputRef, 32 | file: image, 33 | setFile: setImage, 34 | }; 35 | 36 | return { 37 | image, 38 | bottomSheetOpened, 39 | imageUploadProps, 40 | closeBottomSheet, 41 | bottomSheetButtonList, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/presentation/components/common/Input/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { COLOR } from '@styles/common/color'; 4 | import { COMMON_INPUT } from '@styles/common/input'; 5 | import { FONT_STYLES } from '@styles/common/font-style'; 6 | 7 | export const StCommonInput = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | `; 11 | 12 | export const StInputWrapper = styled.form<{ width: string }>` 13 | display: flex; 14 | align-items: center; 15 | width: ${(props) => props.width}; 16 | position: relative; 17 | `; 18 | 19 | export const StInput = styled.input<{ width: string; img?: string; hasButton?: boolean }>` 20 | ${COMMON_INPUT} 21 | height: 52px; 22 | width: ${(props) => props.width}; 23 | background-image: url(${(props) => props.img}); 24 | background-position: left; 25 | background-repeat: no-repeat; 26 | padding: 10px 20px; 27 | ${({ img }) => img && 'padding-left: 34px; background-position: 12px center;'} 28 | ${({ hasButton }) => hasButton && 'padding-right: 56px;'} 29 | `; 30 | 31 | export const StSubmitButton = styled.button` 32 | position: absolute; 33 | right: 16px; 34 | top: calc(50%-26px); 35 | height: 26px; 36 | padding-left: 12px; 37 | border-left: 1px solid ${COLOR.GRAY_3}; 38 | color: ${COLOR.GRAY_7}; 39 | cursor: pointer; 40 | background-color: transparent; 41 | ${FONT_STYLES.M_15_TITLE} 42 | `; 43 | -------------------------------------------------------------------------------- /src/assets/icons/ic_paper_airplane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/presentation/pages/NeososeoForm/Home/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StNeososeoFormHome = styled.div` 6 | width: 100%; 7 | min-height: 100vh; 8 | background-color: ${COLOR.GRAY_1}; 9 | display: grid; 10 | grid-template-rows: 44px 96px auto 110px; 11 | 12 | & > div:not(:nth-child(1)) { 13 | padding: 0 20px; 14 | } 15 | 16 | & > div:nth-child(2) { 17 | & img { 18 | width: 60px; 19 | height: 60px; 20 | border-radius: 20px; 21 | } 22 | display: flex; 23 | gap: 16px; 24 | align-self: flex-end; 25 | & > div { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | gap: 8px; 30 | & > div:nth-child(1) { 31 | color: ${COLOR.GRAY_8}; 32 | ${FONT_STYLES.SB_20_TITLE} 33 | } 34 | & > div:nth-child(2) { 35 | color: ${COLOR.GRAY_5}; 36 | ${FONT_STYLES.R_14_TITLE} 37 | } 38 | } 39 | } 40 | & > div:nth-child(3) { 41 | display: flex; 42 | height: 100%; 43 | justify-content: center; 44 | align-items: center; 45 | } 46 | `; 47 | 48 | export const StAnswerCount = styled.div` 49 | color: ${COLOR.CORAL_MAIN}; 50 | position: absolute; 51 | bottom: 32px; 52 | font-weight: 500; 53 | font-size: 16px; 54 | `; 55 | -------------------------------------------------------------------------------- /src/presentation/pages/Team/Issue/Keyword/style.ts: -------------------------------------------------------------------------------- 1 | import { ANIMATION } from '@styles/common/animation'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from '@styles/common/font-style'; 4 | import styled from 'styled-components'; 5 | 6 | export const StAbsoluteWrapper = styled.div` 7 | position: absolute; 8 | width: 100%; 9 | min-height: 100vh; 10 | top: 0; 11 | left: 0; 12 | background-color: ${COLOR.WHITE}; 13 | z-index: 300; 14 | animation: ${ANIMATION.SWIPE_FROM_RIGHT} 1s; 15 | `; 16 | 17 | export const StTitleWrapper = styled.div` 18 | padding-left: 20px; 19 | padding-top: 40px; 20 | padding-bottom: 12px; 21 | background-color: ${COLOR.GRAY_1}; 22 | 23 | & > span:nth-child(1) { 24 | color: ${COLOR.CORAL_MAIN}; 25 | } 26 | `; 27 | 28 | export const StWhiteWrapper = styled.div` 29 | padding: 20px 12px; 30 | position: relative; 31 | display: flex; 32 | flex-direction: column; 33 | gap: 10px; 34 | `; 35 | 36 | export const StHeader = styled.div` 37 | padding: 12px; 38 | border-bottom: 1px solid ${COLOR.GRAY_2}; 39 | position: relative; 40 | & > div:nth-child(1) { 41 | color: ${COLOR.GRAY_8}; 42 | text-align: center; 43 | ${FONT_STYLES.SB_17_BODY} 44 | } 45 | & > div:nth-child(2) { 46 | position: absolute; 47 | cursor: pointer; 48 | color: ${COLOR.CORAL_MAIN}; 49 | right: 24px; 50 | top: 13px; 51 | ${FONT_STYLES.M_15_TITLE} 52 | } 53 | `; 54 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaResultComment/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import { COLOR } from '@styles/common/color'; 4 | 5 | export const StNeogaResultComment = styled.div` 6 | & > div:first-child { 7 | margin-bottom: 12px; 8 | 9 | span + span { 10 | margin-left: 4px; 11 | } 12 | 13 | span:first-child { 14 | font-weight: 600; 15 | ${FONT_STYLES.SB_13_TITLE}; 16 | color: ${COLOR.GRAY_6}; 17 | } 18 | 19 | span:not(:first-child) { 20 | ${FONT_STYLES.R_13_TITLE}; 21 | color: ${COLOR.GRAY_5}; 22 | } 23 | } 24 | 25 | & > div:nth-child(2) { 26 | ${FONT_STYLES.R_14_TITLE}; 27 | color: ${COLOR.GRAY_7}; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | margin-bottom: 14px; 32 | line-height: 16px; 33 | } 34 | 35 | & > div:last-child { 36 | padding-bottom: 22px; 37 | 38 | div { 39 | gap: 8px; 40 | } 41 | } 42 | `; 43 | 44 | export const StNeogaNoReply = styled.div` 45 | display: flex; 46 | flex-direction: column; 47 | align-items: center; 48 | color: ${COLOR.GRAY_4}; 49 | font-weight: 600; 50 | font-size: 16px; 51 | line-height: 142.5%; 52 | letter-spacing: -0.01em; 53 | padding-top: 55px; 54 | padding-bottom: 72px; 55 | } 56 | 57 | img { 58 | display: block; 59 | margin-bottom: 16px; 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /src/presentation/components/common/BottomSheet/MyPageEdit/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import BottomSheet from '..'; 4 | import { icEdit } from '@assets/icons'; 5 | 6 | type MyPageEditBottomSheetProps = { 7 | isOpened: boolean; 8 | closeBottomSheet: () => void; 9 | type: 'profile' | 'keyword'; 10 | setIsDeletePage?: (value: boolean) => void; 11 | }; 12 | 13 | function MyPageEditBottomSheet(props: MyPageEditBottomSheetProps) { 14 | const { isOpened, closeBottomSheet, type, setIsDeletePage } = props; 15 | const navigate = useNavigate(); 16 | 17 | const editMyKeyword = () => { 18 | closeBottomSheet(); 19 | setIsDeletePage && setIsDeletePage(true); 20 | }; 21 | 22 | const navigateToEditPage = () => { 23 | navigate(`/mypage/edit`); 24 | }; 25 | 26 | return ( 27 | 48 | ); 49 | } 50 | 51 | export default MyPageEditBottomSheet; 52 | -------------------------------------------------------------------------------- /src/presentation/components/common/ProfileList/index.tsx: -------------------------------------------------------------------------------- 1 | import ProfileItem from '@components/common/ProfileItem'; 2 | import ProfileAddButton from '@components/common/ProfileAddButton'; 3 | import { StProfileList, StItemWrapper } from './style'; 4 | 5 | export interface ProfileList { 6 | id: number; 7 | profileImage?: string; 8 | profileName: string; 9 | } 10 | 11 | interface ProfileListProps { 12 | isSquare: boolean; 13 | profileList: ProfileList[]; 14 | onProfileClick?: (id: number) => void; 15 | onAddClick: () => void; 16 | isAddNeeded?: boolean; 17 | } 18 | 19 | function ProfileList(props: ProfileListProps) { 20 | const { 21 | isSquare, 22 | profileList, 23 | onProfileClick = () => { 24 | return; 25 | }, 26 | onAddClick, 27 | isAddNeeded = true, 28 | } = props; 29 | 30 | return ( 31 | 32 | 33 | {profileList.map(({ id, profileImage, profileName }) => ( 34 | onProfileClick(id)} 41 | /> 42 | ))} 43 | {isAddNeeded && } 44 | 45 | 46 | ); 47 | } 48 | 49 | export default ProfileList; 50 | -------------------------------------------------------------------------------- /src/presentation/pages/Team/Member/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, useParams } from 'react-router-dom'; 2 | import { useQuery } from 'react-query'; 3 | 4 | import { api } from '@api/index'; 5 | import { StTeamMember, StMemberInfo, StHostBox } from './style'; 6 | import { IcBack } from '@assets/icons'; 7 | import { imgEmptyProfile } from '@assets/images'; 8 | 9 | function TeamMember() { 10 | const navigate = useNavigate(); 11 | const { teamID } = useParams(); 12 | 13 | if (teamID === undefined) navigate('/'); 14 | 15 | const { data: teamInfoData } = useQuery(['teamDetailData', teamID], () => 16 | api.teamService.getTeamInfo(Number(teamID)), 17 | ); 18 | 19 | return ( 20 | 21 | 22 | navigate(-1)} /> 23 | 팀원 목록 24 | 25 | 26 | {teamInfoData && 27 | teamInfoData.teamMemberList.map( 28 | ({ id, profileImage, profileName, profileId, isHost }) => ( 29 | 30 | 31 | 32 | {profileName} 33 | @{profileId} 34 | 35 | {isHost ? '팀장' : '팀원'} 36 | 37 | ), 38 | )} 39 | 40 | 41 | ); 42 | } 43 | 44 | export default TeamMember; 45 | -------------------------------------------------------------------------------- /src/assets/images/img_computer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/presentation/components/NeogaResultCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { imgEmptyProfile } from '@assets/images'; 2 | import { StNeogaResultCard, StNeogaCardHeader, StNeogaCardLine, StNeogaNoReply } from './style'; 3 | import NeogaResultComment from '@components/NeogaResultComment'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { NeogaAnswerList } from '@api/types/neoga'; 6 | 7 | interface NeogaResultCardProps { 8 | id: number; 9 | title: string; 10 | darkIconImage: string; 11 | createdAt: string; 12 | answer?: NeogaAnswerList[]; 13 | } 14 | 15 | function NeogaResultCard(props: NeogaResultCardProps) { 16 | const { id, title, darkIconImage, createdAt, answer } = props; 17 | const navigate = useNavigate(); 18 | 19 | return ( 20 | navigate(`/neoga/${id}/detail/form`)}> 21 | 22 | 23 | 24 | {title} 25 | {createdAt} 26 | 27 | 28 | 29 | 30 | {answer && answer.length !== 0 ? ( 31 | answer.map((answer) => ) 32 | ) : ( 33 | 34 | 아직 답변이 없어요 35 | 링크를 공유해 답변을 받아보세요 36 | 37 | )} 38 | 39 | 40 | ); 41 | } 42 | 43 | export default NeogaResultCard; 44 | -------------------------------------------------------------------------------- /src/presentation/components/common/IssueCard/style.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { COLOR } from '@styles/common/color'; 3 | import { FONT_STYLES } from '@styles/common/font-style'; 4 | 5 | export const StIssueCard = styled.div<{ issueCardImage?: string }>` 6 | box-shadow: 0px 2px 20px rgba(88, 99, 109, 0.12); 7 | border: 1px solid ${COLOR.GRAY_1}; 8 | border-radius: 20px; 9 | margin-bottom: 14px; 10 | padding: ${(props) => !props.issueCardImage && '24px 20px'}; 11 | cursor: pointer; 12 | 13 | ${(props) => 14 | props.issueCardImage && 15 | css` 16 | & > div:first-child { 17 | height: 96px; 18 | border-radius: 20px 20px 0px 0px; 19 | background: no-repeat center/cover url(${props.issueCardImage}); 20 | } 21 | `} 22 | 23 | & > div:nth-child(2) { 24 | padding: 20px; 25 | } 26 | `; 27 | 28 | export const StCardHeader = styled.div` 29 | margin-bottom: 8px; 30 | 31 | & > span:first-child { 32 | color: ${COLOR.CORAL_MAIN}; 33 | margin-right: 6px; 34 | font-weight: 600; 35 | } 36 | 37 | & > span:last-child { 38 | color: ${COLOR.GRAY_5}; 39 | } 40 | `; 41 | 42 | export const StCardContent = styled.div` 43 | color: ${COLOR.GRAY_8}; 44 | margin-bottom: 28px; 45 | line-height: 160%; 46 | ${FONT_STYLES.SB_16_TITLE}; 47 | white-space: pre-line; 48 | `; 49 | 50 | export const StCardFooter = styled.div` 51 | display: flex; 52 | align-items: center; 53 | justify-content: space-between; 54 | `; 55 | -------------------------------------------------------------------------------- /src/presentation/style/global/index.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { createGlobalStyle } from 'styled-components'; 3 | import reset from 'styled-reset'; 4 | 5 | const GlobalStyle = createGlobalStyle` 6 | ${reset}; 7 | 8 | html, 9 | body { 10 | max-width: 390px; 11 | height: 100%; 12 | margin: 0 auto; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | -webkit-tap-highlight-color : transparent; 18 | } 19 | 20 | button { 21 | cursor: pointer; 22 | border: none; 23 | outline: none; 24 | -webkit-appearance: none; 25 | border-radius: 0; 26 | padding: 0; 27 | } 28 | 29 | input { 30 | -webkit-appearance: none; 31 | -webkit-border-radius: 0; 32 | } 33 | 34 | input:focus { 35 | outline: none; 36 | box-shadow: 0 0 0 2px ${COLOR.CORAL_MAIN}; 37 | } 38 | 39 | body, button, input, textarea { 40 | font-family: Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; 41 | } 42 | 43 | textarea { 44 | box-sizing: border-box; 45 | appearance: none; 46 | -moz-appearance: none; 47 | -webkit-appearance: none; 48 | } 49 | 50 | textarea:focus { 51 | outline: none; 52 | box-shadow: 0 0 0 2px ${COLOR.CORAL_MAIN}; 53 | } 54 | 55 | a { 56 | text-decoration:none; 57 | } 58 | 59 | input[disabled] { 60 | background-color: white; 61 | } 62 | `; 63 | 64 | export default GlobalStyle; 65 | -------------------------------------------------------------------------------- /src/presentation/components/InAppBrowserEscape/index.tsx: -------------------------------------------------------------------------------- 1 | import CommonModal from '@components/common/Modal'; 2 | import React, { useCallback, useEffect, useState } from 'react'; 3 | 4 | const InAppBrowserEscape = () => { 5 | const [isModalOpened, setIsModalOpened] = useState(false); 6 | const openRealBrowser = useCallback(() => { 7 | const isIOS = navigator.userAgent.match(/iPhone|iPad/i); 8 | if (isIOS) { 9 | location.href = 'googlechrome://' + location.href.replace(/https?:\/\//i, ''); 10 | } else { 11 | location.href = 12 | 'intent://' + 13 | location.href.replace(/https?:\/\//i, '') + 14 | '#Intent;scheme=http;package=com.android.chrome;end'; 15 | } 16 | }, []); 17 | useEffect(() => { 18 | const isInAppBrowser = navigator.userAgent.match( 19 | /inapp|NAVER|KAKAOTALK|Snapchat|Line|WirtschaftsWoche|Thunderbird|Instagram|everytimeApp|WhatsApp|Electron|wadiz|AliApp|zumapp|iPhone(.*)Whale|Android(.*)Whale|kakaostory|band|twitter|DaumApps|DaumDevice\/mobile|FB_IAB|FB4A|FBAN|FBIOS|FBSS|SamsungBrowser\/[^1]/i, 20 | ); 21 | if (isInAppBrowser !== null) setIsModalOpened(true); 22 | }, []); 23 | 24 | return ( 25 | setIsModalOpened(false)} 31 | onClickConfirm={openRealBrowser} 32 | isOpened={isModalOpened} 33 | /> 34 | ); 35 | }; 36 | 37 | export default InAppBrowserEscape; 38 | -------------------------------------------------------------------------------- /src/presentation/components/common/Header/style.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '@styles/common/color'; 2 | import { FONT_STYLES } from '@styles/common/font-style'; 3 | import styled from 'styled-components'; 4 | 5 | export const StCommonHeader = styled.div` 6 | width: 100%; 7 | height: 44px; 8 | position: relative; 9 | svg { 10 | cursor: pointer; 11 | } 12 | & > *:first-child { 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | padding: 0 16px; 17 | } 18 | `; 19 | 20 | export const StWrapper = styled.div` 21 | height: 100%; 22 | display: flex; 23 | align-items: center; 24 | & > *:last-child { 25 | margin-left: 10px; 26 | } 27 | `; 28 | 29 | export const StLoginButton = styled.button` 30 | background-color: ${COLOR.CORAL_1}; 31 | border-radius: 13.5px; 32 | ${FONT_STYLES.R_12_TITLE} 33 | line-height: 100%; 34 | letter-spacing: -0.01em; 35 | color: ${COLOR.CORAL_MAIN}; 36 | padding: 7px 12px; 37 | `; 38 | 39 | export const StNotification = styled.div` 40 | width: 7px; 41 | height: 7px; 42 | position: absolute; 43 | top: 14px; 44 | right: 19px; 45 | box-sizing: content-box; 46 | border-radius: 50%; 47 | border: 1px solid #ffffff; 48 | background-color: ${COLOR.CORAL_MAIN}; 49 | `; 50 | 51 | export const StMypageButton = styled.button` 52 | border-radius: 13.5px; 53 | background-color: ${COLOR.CORAL_1}; 54 | color: ${COLOR.CORAL_MAIN}; 55 | padding: 7px 12px; 56 | cursor: pointer; 57 | ${FONT_STYLES.R_12_TITLE} 58 | `; 59 | -------------------------------------------------------------------------------- /src/presentation/components/common/Keyword/MutableList/index.tsx: -------------------------------------------------------------------------------- 1 | import { Keyword } from '@api/types/user'; 2 | import KeywordItem from '../Item'; 3 | import { StKeywordListLayout } from '../style'; 4 | import { COLOR } from '@styles/common/color'; 5 | 6 | interface MutableKeywordListProps { 7 | keywordList: Keyword[]; 8 | deleteKeyword?: (keyword: Keyword) => void; 9 | setIsOpenModal?: (value: boolean) => void; 10 | setKeywordID?: (keywordID: number) => void; 11 | viewMode?: 'linear' | 'flex'; 12 | isMine?: boolean; 13 | } 14 | 15 | function MutableKeywordList(props: MutableKeywordListProps) { 16 | const { 17 | keywordList, 18 | deleteKeyword, 19 | setIsOpenModal, 20 | setKeywordID, 21 | viewMode = 'flex', 22 | isMine, 23 | } = props; 24 | return ( 25 | 26 | {keywordList.map((keyword) => ( 27 | { 34 | setIsOpenModal && setIsOpenModal(true); 35 | setKeywordID && setKeywordID(+keyword.id); 36 | } 37 | : () => deleteKeyword && deleteKeyword(keyword) 38 | } 39 | viewMode={viewMode} 40 | isMine={isMine} 41 | /> 42 | ))} 43 | 44 | ); 45 | } 46 | 47 | export default MutableKeywordList; 48 | --------------------------------------------------------------------------------
이제 내 너가소개서를 받아보세요