├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature.md │ ├── hotfix.md │ └── refactor.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── auto-assign.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .storybook ├── main.ts └── preview.ts ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── releases │ └── yarn-4.5.0.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ ├── api.js │ │ └── unsupported-api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── bin │ │ └── prettier.cjs │ ├── index.cjs │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── README.md ├── emotion.d.ts ├── functions ├── attendanceAdmin │ └── session │ │ └── [id].ts └── tsconfig.json ├── jest.config.ts ├── next.config.js ├── package.json ├── public ├── favicon.ico ├── icons │ └── .keep ├── images │ └── org │ │ ├── imgAboutHeaderInfo.png │ │ ├── imgLatestNewsInfo.png │ │ ├── imgPartInfo.png │ │ ├── imgRecruitHeaderInfo.png │ │ └── imgSubColorInfo.png ├── next.svg ├── thirteen.svg └── vercel.svg ├── src ├── __generated__ │ ├── api.d.ts │ └── org-types │ │ ├── Admin.ts │ │ ├── Health.ts │ │ ├── Homepage.ts │ │ ├── Notification.ts │ │ ├── Projects.ts │ │ ├── Semesters.ts │ │ ├── data-contracts.ts │ │ └── http-client.ts ├── __test__ │ └── example.test.tsx ├── assets │ ├── asset.d.ts │ └── icons │ │ ├── IcAlarmMenu.svg │ │ ├── IcAttendanceMenu.svg │ │ ├── IcBannerMenu.svg │ │ ├── IcCheckBox.tsx │ │ ├── IcDate.svg │ │ ├── IcDeleteFile.svg │ │ ├── IcDropdownCheck.svg │ │ ├── IcEdit.svg │ │ ├── IcGoPrev.svg │ │ ├── IcModalClose.svg │ │ ├── IcMore.svg │ │ ├── IcNewDropdown.svg │ │ ├── IcOrgMenu.svg │ │ ├── IcPaginationLeft.svg │ │ ├── IcPaginationRight.svg │ │ ├── IcPlace.svg │ │ ├── IcTrash.svg │ │ ├── IcUpload.svg │ │ ├── SoptLogos │ │ ├── AndSoptLogo.svg │ │ ├── AtSoptLogo.svg │ │ ├── DoSoptLogo.svg │ │ ├── GoSoptLogo.svg │ │ ├── NowSoptLogo.svg │ │ ├── SoptMainLogo.svg │ │ └── index.ts │ │ ├── calendar_big.svg │ │ └── index.ts ├── components │ ├── alarmAdmin │ │ ├── AlarmList │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── CreateAlarmModal │ │ │ ├── DatePickerSelect.tsx │ │ │ ├── LabeledComponent.tsx │ │ │ ├── index.tsx │ │ │ ├── style.ts │ │ │ ├── type.ts │ │ │ └── utils.ts │ │ └── ShowAlarmModal │ │ │ ├── index.tsx │ │ │ └── style.ts │ ├── attendanceAdmin │ │ ├── session │ │ │ ├── AttendanceModal │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── CreateSessionModal │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── SessionList │ │ │ │ ├── SessionDetailModal │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── SessionListFooter │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ └── totalScore │ │ │ ├── MemberDetail │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ │ └── MemberList │ │ │ ├── index.tsx │ │ │ └── style.ts │ ├── bannerAdmin │ │ ├── BannerEditButton.tsx │ │ ├── BannerImageRegister.tsx │ │ ├── BannerList │ │ │ └── BannerList.tsx │ │ ├── BannerTag │ │ │ └── BannerTag.tsx │ │ ├── ContentTypeField.tsx │ │ ├── CountTag │ │ │ └── CountTag.tsx │ │ ├── CreateBannerModal.tsx │ │ ├── DateRangeField.tsx │ │ ├── DeleteBannerButton.tsx │ │ ├── Header │ │ │ └── Header.tsx │ │ ├── ImageDropZone.tsx │ │ ├── LinkField.tsx │ │ ├── LocationField.tsx │ │ ├── PublisherField.tsx │ │ ├── form │ │ │ ├── Calendar │ │ │ │ └── index.tsx │ │ │ ├── ErrorMessage │ │ │ │ └── index.tsx │ │ │ ├── FormController │ │ │ │ └── index.tsx │ │ │ └── HelpMessage │ │ │ │ └── index.tsx │ │ ├── types │ │ │ ├── api.ts │ │ │ └── form.ts │ │ └── utils │ │ │ ├── converUrlToFile.ts │ │ │ ├── getBannerStatus.ts │ │ │ ├── getBannerType.ts │ │ │ └── getImageSize.ts │ ├── common │ │ ├── AttendanceChip │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Button │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Chip │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── DropDown │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── FilterButton │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── FloatingButton │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Footer │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Form │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Header │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── HelperText │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Input │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Layout │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── ListActionButton │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── ListWrapper │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Loading │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Nav │ │ │ ├── GenerationDropDown │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── OptionTemplate │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── PartFilter │ │ │ └── index.tsx │ │ ├── Selector │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── icons │ │ │ └── IcDropDown.tsx │ │ ├── inputContainer │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ └── modal │ │ │ ├── ModalFooter │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ │ ├── ModalHeader │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ │ ├── index.tsx │ │ │ └── style.ts │ ├── devTools │ │ ├── AdminContextProvider │ │ │ └── index.tsx │ │ └── AdminStatus │ │ │ ├── index.tsx │ │ │ └── style.ts │ ├── icons │ │ └── IcDropdown.tsx │ ├── org │ │ ├── OrgAdmin │ │ │ ├── AboutSection │ │ │ │ ├── CoreValue │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── Curriculum │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── Executives │ │ │ │ │ ├── ExecInfo.tsx │ │ │ │ │ ├── SNSInput.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── HeaderBanner │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ ├── assets │ │ │ │ │ ├── IcBehanceLogo.tsx │ │ │ │ │ ├── IcGithubLogo.tsx │ │ │ │ │ ├── IcLinkedinLogo.tsx │ │ │ │ │ └── IcMailLogo.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── CommonSection │ │ │ │ ├── BrandingColor.tsx │ │ │ │ ├── BrandingSubColor.tsx │ │ │ │ ├── ColorInputField.tsx │ │ │ │ ├── GenerationInformation.tsx │ │ │ │ ├── RecruitSchedule.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── style.ts │ │ │ │ └── utils.ts │ │ │ ├── HomeSection │ │ │ │ ├── ButtonSection.tsx │ │ │ │ ├── HomeSection.tsx │ │ │ │ ├── ImageInput.tsx │ │ │ │ ├── LiveAppliedButton.tsx │ │ │ │ ├── Modal.style.ts │ │ │ │ ├── Modal.tsx │ │ │ │ ├── NewsItem.tsx │ │ │ │ ├── NewsSection.tsx │ │ │ │ ├── PartIntroSection.tsx │ │ │ │ ├── SampleView.tsx │ │ │ │ ├── api.ts │ │ │ │ ├── constant.ts │ │ │ │ ├── queries.ts │ │ │ │ └── style.ts │ │ │ ├── MyDropzone │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── PartCategory │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── RecruitSection │ │ │ │ ├── Fna.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── PartCurriculum.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── api.ts │ │ │ ├── assets │ │ │ │ ├── RequiredIcon.tsx │ │ │ │ ├── SubmitIcon.tsx │ │ │ │ └── imgSubColorInfo.png │ │ │ ├── common │ │ │ │ ├── ActionModal │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.ts │ │ │ │ └── Modal │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── style.ts │ │ │ │ │ └── useModal.ts │ │ │ ├── hooks.ts │ │ │ ├── index.tsx │ │ │ ├── style.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ └── RecruitAdmin │ │ │ ├── index.tsx │ │ │ └── style.ts │ └── session │ │ └── Select │ │ ├── index.tsx │ │ └── style.ts ├── configs │ └── config.ts ├── constants │ └── index.ts ├── data │ ├── queryData.ts │ └── sessionData.ts ├── hooks │ ├── useBooleanState.ts │ ├── useCreateSession.ts │ ├── useInput.ts │ ├── useObserver.ts │ ├── useRecoilGenerationSSR.ts │ └── useUnauthorizedStatus.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── alarmAdmin │ │ └── index.tsx │ ├── api │ │ └── hello.ts │ ├── attendanceAdmin │ │ ├── session │ │ │ ├── [id].tsx │ │ │ └── index.tsx │ │ └── totalScore │ │ │ └── index.tsx │ ├── bannerAdmin │ │ └── index.tsx │ ├── index.tsx │ └── org │ │ ├── org-admin │ │ └── index.tsx │ │ └── recruit-admin │ │ └── index.tsx ├── recoil │ └── atom.ts ├── services │ └── api │ │ ├── alarm │ │ ├── index.ts │ │ └── query.ts │ │ ├── attendance │ │ ├── index.ts │ │ └── query.ts │ │ ├── auth │ │ └── index.ts │ │ ├── banner │ │ ├── index.ts │ │ └── query.ts │ │ ├── client.ts │ │ ├── lecture │ │ ├── index.ts │ │ └── query.ts │ │ └── member │ │ ├── index.ts │ │ └── query.ts ├── store │ └── globalStore.ts ├── styles │ ├── global.ts │ ├── mediaQuery.ts │ └── theme.ts ├── types │ └── global.d.ts └── utils │ ├── alarm.ts │ ├── auth.ts │ ├── date.ts │ ├── generation.ts │ ├── index.ts │ ├── nav.ts │ ├── org.ts │ ├── putObject.ts │ ├── session.ts │ ├── translator.tsx │ └── zIndex.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:prettier/recommended", 5 | "plugin:react-hooks/recommended", 6 | "prettier", 7 | "plugin:storybook/recommended" 8 | ], 9 | "plugins": ["prettier", "simple-import-sort", "react-hooks"], 10 | "rules": { 11 | // React hooks 규칙을 검사하여 잘 사용했는지 확인하고 경고합니다. 12 | "react-hooks/rules-of-hooks": "error", 13 | 14 | // useEffect나 useCallback 등에서 사용하는 의존성 배열(deps)이 모든 상황에 대해 모두 포함되었는지 확인하고 경고합니다. 15 | "react-hooks/exhaustive-deps": "warn", 16 | 17 | // Prettier 에러가 발생하면 바로 에러를 발생시킵니다. 18 | "prettier/prettier": "error", 19 | 20 | // 쌍 따옴표 대신 홑 따옴표를 사용하는 것을 강제합니다. 21 | "quotes": ["error", "single"], 22 | 23 | // 세미콜론을 항상 사용하도록 강제합니다. 24 | "semi": ["error", "always"], 25 | 26 | // 객체나 배열의 마지막 요소 뒤에 항상 쉼표를 사용합니다. 27 | "comma-dangle": ["error", "always-multiline"], 28 | 29 | // 빈 줄을 허용하지 않습니다. 최대 빈 줄 수는 1개입니다. 30 | "no-multiple-empty-lines": [ 31 | "error", 32 | { 33 | "max": 1 34 | } 35 | ], 36 | 37 | // 쉼표 앞에는 공백을 사용하지 않도록 합니다. 쉼표 뒤에는 공백을 사용하도록 합니다. 38 | "comma-spacing": [ 39 | "error", 40 | { 41 | "before": false, 42 | "after": true 43 | } 44 | ], 45 | 46 | // 탭을 사용하지 않도록 합니다. 들여쓰기에서는 탭을 허용합니다. 47 | "no-tabs": [ 48 | "error", 49 | { 50 | "allowIndentationTabs": true 51 | } 52 | ], 53 | 54 | // 계산된 속성 표기법에서 괄호 안에 공백을 사용하지 않도록 합니다. 55 | "computed-property-spacing": ["error", "never"], 56 | 57 | // 괄호 안에 공백을 사용하지 않도록 합니다. 58 | "space-in-parens": ["error", "never"], 59 | 60 | // 파일 끝에 항상 개행문자를 사용하도록 합니다. 61 | "eol-last": ["error", "always"], 62 | 63 | // import 구문을 간단하게 정리(sort)하도록 합니다. 64 | "simple-import-sort/imports": "error", 65 | 66 | // export 구문을 간단하게 정리(sort)하도록 합니다. 67 | "simple-import-sort/exports": "error" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /src/__generated__/* linguist-generated=true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: bug report 3 | about: 버그 보고하기 4 | title: "[BUG] bug 내용" 5 | labels: "\U0001F41B bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## 🌴 작업 브랜치 13 | 14 | ## 🐛 BUG 개요 15 | 16 | ## 🚧 BUG 리포트 17 | 18 | ## ✅ TODO 및 진행현황 19 | 20 | - [ ] todo1 21 | - [ ] todo2 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: feature 3 | about: 새로운 기능 구현하기 4 | title: "[FEATURE] view_feature 내용" 5 | labels: "✨ feature" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## 🌴 작업 브랜치 13 | 14 | ## 💼 TASK 개요 15 | 16 | ## ✅ TODO 및 진행현황 17 | 18 | - [ ] todo1 19 | - [ ] todo2 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/hotfix.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: hotfix 3 | about: 빠르게 수정하기 4 | title: "[HOTFIX] hotfix 내용" 5 | labels: "\U0001F525 hotfix" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## 🌴 작업 브랜치 13 | 14 | ## 💼 TASK 개요 15 | 16 | ## ✅ TODO 및 진행현황 17 | 18 | - [ ] todo1 19 | - [ ] todo2 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: refactor 3 | about: 리팩토링하기 4 | title: "[REFACTOR] refactor 내용" 5 | labels: "⚒️ refactor" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## 🌴 작업 브랜치 13 | 14 | ## 🔨 Refactor 개요 15 | 16 | ## ✅ TODO 및 진행현황 17 | 18 | - [ ] todo1 19 | - [ ] todo2 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## ✨ 구현 기능 명세 4 | 5 | 6 | 7 | ## ✅ PR Point 8 | 9 | 10 | 11 | ## 😭 어려웠던 점 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: Auto Assign PR Author 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | auto-assign: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Install GitHub CLI 16 | run: | 17 | sudo apt-get update 18 | sudo apt-get install -y gh 19 | 20 | - name: Auto-assign PR author 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | run: | 24 | gh auth setup-git 25 | gh pr edit ${{ github.event.pull_request.number }} --add-assignee ${{ github.event.pull_request.user.login }} 26 | -------------------------------------------------------------------------------- /.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 | .pnp.cjs 8 | .pnp.loader.mjs 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | # webstorm 41 | .idea 42 | 43 | # env 44 | .env 45 | 46 | # yarn berry 47 | .yarn/* 48 | !.yarn/cache 49 | !.yarn/patches 50 | !.yarn/plugins 51 | !.yarn/releases 52 | !.yarn/sdks 53 | !.yarn/versions -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.15.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 80, 5 | "trailingComma": "all", 6 | "bracketSameLine": true, 7 | "singleQuote": true, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs'; 2 | const config: StorybookConfig = { 3 | stories: [ 4 | '../src/**/*.stories.@(js|jsx|ts|tsx)', 5 | '../src/components/**/*.stories.@(js|jsx|ts|tsx)', 6 | ], 7 | addons: [ 8 | '@storybook/addon-links', 9 | '@storybook/addon-essentials', 10 | '@storybook/addon-interactions', 11 | ], 12 | framework: { 13 | name: '@storybook/nextjs', 14 | options: {}, 15 | }, 16 | docs: { 17 | autodocs: 'tag', 18 | }, 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: '^on[A-Z].*' }, 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "search.exclude": { 8 | "**/.yarn": true, 9 | "**/.pnp.*": true 10 | }, 11 | "eslint.nodePath": ".yarn/sdks", 12 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", 13 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 14 | "typescript.enablePromptUseWorkspaceTsdk": true 15 | } 16 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require eslint/bin/eslint.js 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real eslint/bin/eslint.js your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require eslint 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real eslint your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`eslint`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/unsupported-api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require eslint/use-at-your-own-risk 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real eslint/use-at-your-own-risk your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.57.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "eslint": "./bin/eslint.js" 8 | }, 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": "./lib/api.js", 12 | "./use-at-your-own-risk": "./lib/unsupported-api.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/bin/prettier.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require prettier/bin/prettier.cjs 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real prettier/bin/prettier.cjs your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`prettier/bin/prettier.cjs`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require prettier 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real prettier your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`prettier`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "3.3.3-sdk", 4 | "main": "./index.cjs", 5 | "type": "commonjs", 6 | "bin": "./bin/prettier.cjs" 7 | } 8 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript/bin/tsc 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript/bin/tsc your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript/bin/tsserver 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript/bin/tsserver your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript/lib/tsc.js 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript/lib/tsc.js your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "5.5.4-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "tsc": "./bin/tsc", 8 | "tsserver": "./bin/tsserver" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.5.0.cjs 2 | packageExtensions: 3 | '@sopt-makers/ui@2.0.2': 4 | peerDependencies: 5 | react-dom: '^18.2.0' 6 | '@storybook/addon-controls@8.0.4': 7 | peerDependencies: 8 | react: '^18.2.0' 9 | react-dom: '^18.2.0' 10 | '@storybook/addon-essentials@8.0.4': 11 | peerDependencies: 12 | react: '^18.2.0' 13 | react-dom: '^18.2.0' 14 | '@storybook/core-server@8.0.4': 15 | peerDependencies: 16 | react: '^18.2.0' 17 | react-dom: '^18.2.0' 18 | '@storybook/cli@8.0.4': 19 | peerDependencies: 20 | react: '^18.2.0' 21 | react-dom: '^18.2.0' 22 | '@storybook/manager-api@8.0.4': 23 | peerDependencies: 24 | react: '^18.2.0' 25 | react-dom: '^18.2.0' 26 | '@types/react-datepicker@6.2.0': 27 | peerDependencies: 28 | react: '^18.2.0' 29 | react-dom: '^18.2.0' 30 | '@typescript-eslint/utils@5.62.0': 31 | peerDependencies: 32 | typescript: '^5.4.4' 33 | 'eslint-plugin-storybook@0.8.0': 34 | peerDependencies: 35 | react: '^18.2.0' 36 | react-dom: '^18.2.0' 37 | typescript: '^5.4.4' 38 | 'storybook@8.0.4': 39 | peerDependencies: 40 | react: '^18.2.0' 41 | react-dom: '^18.2.0' 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /emotion.d.ts: -------------------------------------------------------------------------------- 1 | import '@emotion/react'; 2 | 3 | declare module '@emotion/react' { 4 | export interface Theme { 5 | color: { 6 | main: { 7 | orange50: string; 8 | blue50: string; 9 | purple100: string; 10 | purple40: string; 11 | purpledim100: string; 12 | purpledim20: string; 13 | newBlue: string; 14 | }; 15 | sub: { 16 | red: string; 17 | green: string; 18 | yellow: string; 19 | }; 20 | grayscale: { 21 | black100: string; 22 | black80: string; 23 | black60: string; 24 | black40: string; 25 | realwhite: string; 26 | white100: string; 27 | gray10: string; 28 | gray20: string; 29 | gray30: string; 30 | gray40: string; 31 | gray60: string; 32 | gray80: string; 33 | gray100: string; 34 | }; 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /functions/attendanceAdmin/session/[id].ts: -------------------------------------------------------------------------------- 1 | export const onRequest: PagesFunction = async (context) => { 2 | const { next, params } = context; 3 | 4 | if (/\d+/.test(`${params.id}`)) { 5 | return next('/attendanceAdmin/session/[id]'); 6 | } 7 | 8 | return next(); 9 | }; 10 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "types": ["@cloudflare/workers-types"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | import nextJest from 'next/jest'; 3 | 4 | const createJestConfig = nextJest({ 5 | dir: './', 6 | }); 7 | 8 | const customJestConfig: Config = { 9 | // setupFilesAfterEnv: ['/jest.setup.js'], 10 | 11 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 12 | // moduleDirectories: ['node_modules', '/'], 13 | 14 | // Handle module aliases 15 | moduleNameMapper: { 16 | '^@/(.*)$': '/src/$1', 17 | }, 18 | 19 | testEnvironment: 'jest-environment-jsdom', 20 | }; 21 | 22 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 23 | export default createJestConfig(customJestConfig); 24 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | eslint: { 5 | ignoreDuringBuilds: true, 6 | }, 7 | output: 'export', 8 | webpack(config) { 9 | config.module.rules.push({ 10 | test: /\.svg$/, 11 | issuer: { 12 | and: [/\.(js|ts)x?$/], 13 | }, 14 | use: ['@svgr/webpack'], 15 | }); 16 | 17 | return config; 18 | }, 19 | images: { 20 | unoptimized: true, 21 | remotePatterns: [ 22 | { 23 | protocol: 'https', 24 | hostname: 's3.ap-northeast-2.amazonaws.com', 25 | port: '', 26 | pathname: '/sopt.org/admin/**', 27 | }, 28 | ], 29 | }, 30 | }; 31 | 32 | module.exports = nextConfig; 33 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/public/icons/.keep -------------------------------------------------------------------------------- /public/images/org/imgAboutHeaderInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/public/images/org/imgAboutHeaderInfo.png -------------------------------------------------------------------------------- /public/images/org/imgLatestNewsInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/public/images/org/imgLatestNewsInfo.png -------------------------------------------------------------------------------- /public/images/org/imgPartInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/public/images/org/imgPartInfo.png -------------------------------------------------------------------------------- /public/images/org/imgRecruitHeaderInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/public/images/org/imgRecruitHeaderInfo.png -------------------------------------------------------------------------------- /public/images/org/imgSubColorInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/public/images/org/imgSubColorInfo.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/__generated__/org-types/Health.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* 4 | * --------------------------------------------------------------- 5 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 6 | * ## ## 7 | * ## AUTHOR: acacode ## 8 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 9 | * --------------------------------------------------------------- 10 | */ 11 | 12 | import { HealthCheckData } from './data-contracts'; 13 | import { HttpClient, RequestParams } from './http-client'; 14 | 15 | export class Health< 16 | SecurityDataType = unknown, 17 | > extends HttpClient { 18 | /** 19 | * No description 20 | * 21 | * @tags HealthCheck 22 | * @name HealthCheck 23 | * @request GET:/health 24 | * @response `200` `HealthCheckData` OK 25 | */ 26 | healthCheck = (params: RequestParams = {}) => 27 | this.request({ 28 | path: `/health`, 29 | method: 'GET', 30 | ...params, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/__generated__/org-types/Homepage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* 4 | * --------------------------------------------------------------- 5 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 6 | * ## ## 7 | * ## AUTHOR: acacode ## 8 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 9 | * --------------------------------------------------------------- 10 | */ 11 | 12 | import { GetAboutPageData, GetData, GetMainPageData } from './data-contracts'; 13 | import { HttpClient, RequestParams } from './http-client'; 14 | 15 | export class Homepage< 16 | SecurityDataType = unknown, 17 | > extends HttpClient { 18 | /** 19 | * @description 메인 페이지 데이터를 조회합니다 20 | * 21 | * @tags Homepage 22 | * @name GetMainPage 23 | * @summary 메인 페이지 조회 24 | * @request GET:/homepage 25 | * @response `200` `GetMainPageData` OK 26 | */ 27 | getMainPage = (params: RequestParams = {}) => 28 | this.request({ 29 | path: `/homepage`, 30 | method: 'GET', 31 | ...params, 32 | }); 33 | /** 34 | * @description 지원하기 페이지 데이터를 조회합니다 35 | * 36 | * @tags Homepage 37 | * @name Get 38 | * @summary 지원하기 페이지 조회 39 | * @request GET:/homepage/recruit 40 | * @response `200` `GetData` OK 41 | */ 42 | get = (params: RequestParams = {}) => 43 | this.request({ 44 | path: `/homepage/recruit`, 45 | method: 'GET', 46 | ...params, 47 | }); 48 | /** 49 | * @description 소개 페이지 데이터를 조회합니다 50 | * 51 | * @tags Homepage 52 | * @name GetAboutPage 53 | * @summary 소개 페이지 조회 54 | * @request GET:/homepage/about 55 | * @response `200` `GetAboutPageData` OK 56 | */ 57 | getAboutPage = (params: RequestParams = {}) => 58 | this.request({ 59 | path: `/homepage/about`, 60 | method: 'GET', 61 | ...params, 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/__generated__/org-types/Notification.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* 4 | * --------------------------------------------------------------- 5 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 6 | * ## ## 7 | * ## AUTHOR: acacode ## 8 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 9 | * --------------------------------------------------------------- 10 | */ 11 | 12 | import { GetAllProjectData, RegisterNotificationData } from './data-contracts'; 13 | import { HttpClient, RequestParams } from './http-client'; 14 | 15 | export class Notification< 16 | SecurityDataType = unknown, 17 | > extends HttpClient { 18 | /** 19 | * No description 20 | * 21 | * @tags Notification 22 | * @name RegisterNotification 23 | * @request POST:/notification/register 24 | * @response `200` `RegisterNotificationData` OK 25 | */ 26 | registerNotification = ( 27 | query: { 28 | /** 29 | * 활동 기수 30 | * @example 34 31 | */ 32 | generation: string; 33 | /** 34 | * 이메일 35 | * @example "example@naver.com" 36 | */ 37 | email: string; 38 | }, 39 | params: RequestParams = {}, 40 | ) => 41 | this.request({ 42 | path: `/notification/register`, 43 | method: 'POST', 44 | query: query, 45 | ...params, 46 | }); 47 | /** 48 | * No description 49 | * 50 | * @tags Notification 51 | * @name GetAllProject 52 | * @request GET:/notification/list 53 | * @response `200` `GetAllProjectData` OK 54 | */ 55 | getAllProject = ( 56 | query?: { 57 | /** 58 | * 기수 59 | * @format int32 60 | */ 61 | generation?: number; 62 | }, 63 | params: RequestParams = {}, 64 | ) => 65 | this.request({ 66 | path: `/notification/list`, 67 | method: 'GET', 68 | query: query, 69 | ...params, 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /src/__generated__/org-types/Projects.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* 4 | * --------------------------------------------------------------- 5 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 6 | * ## ## 7 | * ## AUTHOR: acacode ## 8 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 9 | * --------------------------------------------------------------- 10 | */ 11 | 12 | import { GetProjectData, GetProjectsData } from './data-contracts'; 13 | import { HttpClient, RequestParams } from './http-client'; 14 | 15 | export class Projects< 16 | SecurityDataType = unknown, 17 | > extends HttpClient { 18 | /** 19 | * No description 20 | * 21 | * @tags Project 22 | * @name GetProjects 23 | * @summary 프로젝트 정보 전부 가져오기 24 | * @request GET:/projects 25 | * @response `200` `GetProjectsData` OK 26 | */ 27 | getProjects = ( 28 | query?: { 29 | /** 필터링 키워드 */ 30 | filter?: 31 | | 'APPJAM' 32 | | 'SOPKATHON' 33 | | 'SOPTERM' 34 | | 'STUDY' 35 | | 'JOINTSEMINAR' 36 | | 'ETC'; 37 | /** 웹/앱 필터링 */ 38 | platform?: 'WEB' | 'APP'; 39 | /** 40 | * 페이지 41 | * @format int32 42 | * @min 1 43 | * @example 1 44 | */ 45 | pageNo?: number; 46 | /** 47 | * 페이지별 데이터 개수 48 | * @format int32 49 | * @min 1 50 | * @example 10 51 | */ 52 | limit?: number; 53 | }, 54 | params: RequestParams = {}, 55 | ) => 56 | this.request({ 57 | path: `/projects`, 58 | method: 'GET', 59 | query: query, 60 | ...params, 61 | }); 62 | /** 63 | * No description 64 | * 65 | * @tags Project 66 | * @name GetProject 67 | * @summary 특정 프로젝트 정보 가져오기 68 | * @request GET:/projects/{projectId} 69 | * @response `200` `GetProjectData` OK 70 | */ 71 | getProject = (projectId: number, params: RequestParams = {}) => 72 | this.request({ 73 | path: `/projects/${projectId}`, 74 | method: 'GET', 75 | ...params, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /src/__generated__/org-types/Semesters.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* 4 | * --------------------------------------------------------------- 5 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 6 | * ## ## 7 | * ## AUTHOR: acacode ## 8 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 9 | * --------------------------------------------------------------- 10 | */ 11 | 12 | import { GetSemestersData } from './data-contracts'; 13 | import { HttpClient, RequestParams } from './http-client'; 14 | 15 | export class Semesters< 16 | SecurityDataType = unknown, 17 | > extends HttpClient { 18 | /** 19 | * No description 20 | * 21 | * @tags Semester 22 | * @name GetSemesters 23 | * @request GET:/semesters 24 | * @response `200` `GetSemestersData` OK 25 | */ 26 | getSemesters = ( 27 | query: { 28 | /** @format int32 */ 29 | limit: number; 30 | /** 31 | * @format int32 32 | * @default 1 33 | */ 34 | page?: number; 35 | }, 36 | params: RequestParams = {}, 37 | ) => 38 | this.request({ 39 | path: `/semesters`, 40 | method: 'GET', 41 | query: query, 42 | ...params, 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/__test__/example.test.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@emotion/react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { RecoilRoot } from 'recoil'; 4 | 5 | import Home from '@/pages/index'; 6 | import theme from '@/styles/theme'; 7 | 8 | describe('Home', () => { 9 | test('

내부에 "웹 어드민 렛츠고."가 있는가?', () => { 10 | render( 11 | 12 | 13 | 14 | 15 | , 16 | ); 17 | const h1Text = screen.getByRole('heading').innerHTML; 18 | expect(h1Text).toBe('웹 어드민 렛츠고.'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/assets/asset.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | declare module '*.png'; 3 | declare module '*.jpeg'; 4 | declare module '*.gif'; 5 | declare module '*.svg' { 6 | import React = require('react'); 7 | export const ReactComponent: React.FunctionComponent< 8 | React.SVGProps 9 | >; 10 | const src: string; 11 | export default src; 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/IcAlarmMenu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/IcAttendanceMenu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/IcBannerMenu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/IcCheckBox.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | isChecked?: boolean; 3 | onClick?: () => void; 4 | } 5 | 6 | function IcCheckBox(props: Props) { 7 | const { isChecked = false, onClick } = props; 8 | 9 | return ( 10 | 17 | 18 | 26 | 34 | 35 | ); 36 | } 37 | 38 | export default IcCheckBox; 39 | -------------------------------------------------------------------------------- /src/assets/icons/IcDate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/IcDeleteFile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/IcDropdownCheck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/IcEdit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/IcGoPrev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/IcModalClose.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/IcMore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/icons/IcNewDropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/IcOrgMenu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/IcPaginationLeft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/IcPaginationRight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/IcPlace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/IcTrash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/IcUpload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/icons/SoptLogos/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AndSoptLogo } from './AndSoptLogo.svg'; 2 | export { default as AtSoptLogo } from './AtSoptLogo.svg'; 3 | export { default as DoSoptLogo } from './DoSoptLogo.svg'; 4 | export { default as GoSoptLogo } from './GoSoptLogo.svg'; 5 | export { default as NowSoptLogo } from './NowSoptLogo.svg'; 6 | export { default as SoptMainLogo } from './SoptMainLogo.svg'; 7 | -------------------------------------------------------------------------------- /src/assets/icons/calendar_big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as IcAlarmMenu } from './IcAlarmMenu.svg'; 2 | export { default as IcAttendanceMenu } from './IcAttendanceMenu.svg'; 3 | export { default as IcBannerMenu } from './IcBannerMenu.svg'; 4 | export { default as IcCheckBox } from './IcCheckBox'; 5 | export { default as IcDeleteFile } from './IcDeleteFile.svg'; 6 | export { default as IcEdit } from './IcEdit.svg'; 7 | export { default as IcGoPrev } from './IcGoPrev.svg'; 8 | export { default as IcModalClose } from './IcModalClose.svg'; 9 | export { default as IcNewDropdown } from './IcNewDropdown.svg'; 10 | export { default as IcOrgMenu } from './IcOrgMenu.svg'; 11 | export { default as IcPaginationLeft } from './IcPaginationLeft.svg'; 12 | export { default as IcPaginationRight } from './IcPaginationRight.svg'; 13 | export { default as IcTrash } from './IcTrash.svg'; 14 | export { default as IcUpload } from './IcUpload.svg'; 15 | -------------------------------------------------------------------------------- /src/components/alarmAdmin/CreateAlarmModal/DatePickerSelect.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | import { IconChevronDown } from '@sopt-makers/icons'; 5 | import React from 'react'; 6 | 7 | interface DatePickerSelectProps { 8 | selectedDate: string | null; 9 | placeholder: string; 10 | open: boolean; 11 | } 12 | 13 | function DatePickerSelect({ 14 | selectedDate, 15 | placeholder, 16 | open, 17 | }: DatePickerSelectProps) { 18 | const selectedLabel = selectedDate ? selectedDate : placeholder; 19 | 20 | return ( 21 | 22 | 23 | 24 | {selectedLabel} 25 | 26 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default DatePickerSelect; 40 | 41 | const DatePickerSelectButton = styled.button` 42 | width: 100%; 43 | height: 100%; 44 | background: none; 45 | border: none; 46 | padding: 0; 47 | margin: 0; 48 | cursor: pointer; 49 | `; 50 | 51 | const DatePickerSelectWrapper = styled.div` 52 | width: 100%; 53 | height: 100%; 54 | 55 | ${fontsObject.BODY_2_16_M}; 56 | color: ${colors.white}; 57 | 58 | border-radius: 10px; 59 | 60 | border: 1px solid transparent; 61 | padding: 11px 16px; 62 | display: flex; 63 | justify-content: space-between; 64 | align-items: center; 65 | gap: 12px; 66 | cursor: pointer; 67 | transition: border 0.2s; 68 | 69 | background-color: ${colors.gray700}; 70 | 71 | &:focus { 72 | border: 1px solid ${colors.gray200}; 73 | } 74 | `; 75 | 76 | const DatePickerSelectLabel = styled.p<{ isSelected: boolean }>` 77 | color: ${({ isSelected }) => !isSelected && colors.gray300}; 78 | `; 79 | -------------------------------------------------------------------------------- /src/components/alarmAdmin/CreateAlarmModal/LabeledComponent.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | import { ReactNode } from 'react'; 5 | 6 | interface LabelProps { 7 | labelText: string; 8 | desc?: string; 9 | children?: ReactNode; 10 | } 11 | 12 | function LabeledComponent({ labelText, desc, children }: LabelProps) { 13 | return ( 14 | 15 | 16 | {labelText} 17 | * 18 | 19 | {desc && ( 20 | 21 | {desc} 22 | 23 | )} 24 | {children} 25 | 26 | ); 27 | } 28 | 29 | export default LabeledComponent; 30 | 31 | const LabeledComponentWrapper = styled.div` 32 | display: flex; 33 | flex-direction: column; 34 | `; 35 | 36 | const LabelWrapper = styled.label` 37 | display: flex; 38 | gap: 0.4rem; 39 | `; 40 | 41 | const DescWrapper = styled.label` 42 | margin: 0.8rem 0; 43 | `; 44 | 45 | const DescText = styled.span` 46 | ${fontsObject.LABEL_4_12_SB} 47 | color: ${colors.gray300}; 48 | `; 49 | 50 | const LabelText = styled.span` 51 | ${fontsObject.LABEL_3_14_SB} 52 | color: ${colors.white}; 53 | `; 54 | 55 | const RequiredStar = styled.span` 56 | ${fontsObject.LABEL_3_14_SB} 57 | color: ${colors.secondary}; 58 | `; 59 | -------------------------------------------------------------------------------- /src/components/alarmAdmin/CreateAlarmModal/type.ts: -------------------------------------------------------------------------------- 1 | export type SendTargetType = '전체' | '활동 회원' | 'CSV 첨부'; 2 | export type requestTargetType = 'ALL' | 'ACTIVE' | 'CSV'; 3 | 4 | export type AttachOptionType = '웹 링크' | '앱 내 딥링크'; 5 | export type requestLinkType = 'WEB' | 'APP' | null; 6 | 7 | export interface ISendTargetOptions { 8 | label: SendTargetType; 9 | value: SendTargetType; 10 | } 11 | 12 | export type SendPartType = 13 | | '전체' 14 | | '기획' 15 | | '디자인' 16 | | '서버' 17 | | 'iOS' 18 | | '안드로이드' 19 | | '웹' 20 | | ''; 21 | 22 | export interface ISendPartOptions { 23 | label: SendPartType; 24 | value: SendPartType; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/alarmAdmin/ShowAlarmModal/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@sopt-makers/colors'; 4 | import { fontsObject } from '@sopt-makers/fonts'; 5 | 6 | export const StAlarmModalWrapper = styled.section` 7 | width: 64rem; 8 | `; 9 | 10 | export const StAlarmModalBody = styled.main` 11 | padding: 2.6rem 3rem; 12 | display: flex; 13 | flex-direction: column; 14 | gap: 1.6rem; 15 | 16 | & > div { 17 | display: flex; 18 | align-items: flex-start; 19 | gap: 20px; 20 | } 21 | label { 22 | ${fontsObject.LABEL_3_14_SB}; 23 | color: ${colors.white}; 24 | margin-bottom: 8px; 25 | display: block; 26 | } 27 | `; 28 | 29 | export const StAlarmModalFooter = styled.footer` 30 | padding: 2.5rem 3.2rem; 31 | display: flex; 32 | justify-content: flex-end; 33 | margin-top: 1.8rem; 34 | `; 35 | 36 | export const StTextField = styled.div<{ full?: boolean; textarea?: boolean }>` 37 | width: ${({ full }) => (full ? '100%' : 'auto')}; 38 | 39 | p { 40 | ${fontsObject.BODY_2_16_M}; 41 | color: ${colors.white}; 42 | background-color: #2e2e35; 43 | padding: 11px 16px; 44 | border-radius: 10px; 45 | min-width: 180px; 46 | line-height: 26px; 47 | min-height: ${({ textarea }) => (textarea ? '128px' : '48px')}; 48 | } 49 | `; 50 | 51 | export const StRadioWrap = styled.div` 52 | display: flex; 53 | flex-direction: column; 54 | 55 | div { 56 | display: flex; 57 | align-items: center; 58 | gap: 6px; 59 | } 60 | `; 61 | 62 | export const StLink = styled.a<{ linkType: LINK_TYPE }>` 63 | ${fontsObject.LABEL_3_14_SB}; 64 | color: ${colors.gray200}; 65 | margin-top: 8px; 66 | display: flex; 67 | align-items: center; 68 | gap: 4px; 69 | pointer-events: none; 70 | 71 | ${({ linkType }) => 72 | linkType === 'WEB' && 73 | css` 74 | cursor: pointer; 75 | text-decoration: underline; 76 | pointer-events: visible; 77 | `} 78 | `; 79 | -------------------------------------------------------------------------------- /src/components/attendanceAdmin/session/AttendanceModal/style.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | 5 | export const StAttendanceModal = styled.div` 6 | & > div { 7 | width: 90rem; 8 | padding: 3.2rem 4rem 0 4rem; 9 | } 10 | .timer { 11 | text-align: center; 12 | font-size: 4.8rem; 13 | font-style: normal; 14 | font-weight: 600; 15 | line-height: 140%; /* 6.72rem */ 16 | letter-spacing: -0.096rem; 17 | color: ${colors.gray10}; 18 | margin-bottom: 2rem; 19 | &-warn { 20 | color: ${colors.error}; 21 | } 22 | } 23 | .code-wrapper { 24 | display: flex; 25 | justify-content: center; 26 | gap: 1rem; 27 | margin-bottom: 5.6rem; 28 | & > div { 29 | width: 8.2rem; 30 | height: 11.2rem; 31 | border-radius: 0.8rem; 32 | background-color: ${colors.gray700}; 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | & > p { 37 | color: ${colors.gray10}; 38 | text-align: center; 39 | font-feature-settings: 40 | 'clig' off, 41 | 'liga' off; 42 | font-family: SUIT; 43 | font-size: 4rem; 44 | font-style: normal; 45 | font-weight: 700; 46 | line-height: 160%; /* 6.4rem */ 47 | letter-spacing: -0.08rem; 48 | } 49 | } 50 | } 51 | & > footer { 52 | display: flex; 53 | justify-content: space-between; 54 | align-items: center; 55 | p { 56 | ${fontsObject.TITLE_7_14_SB} 57 | color: ${colors.error}; 58 | } 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/attendanceAdmin/session/SessionListFooter/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRecoilValue } from 'recoil'; 2 | 3 | import Button from '@/components/common/Button'; 4 | import { currentGenerationState } from '@/recoil/atom'; 5 | import { ACTIVITY_GENERATION } from '@/utils/generation'; 6 | 7 | import { StFooterWrapper } from './style'; 8 | 9 | interface Props { 10 | onClick: () => void; 11 | } 12 | 13 | function SessionListFooter(props: Props) { 14 | const { onClick } = props; 15 | const currentGeneration = useRecoilValue(currentGenerationState); 16 | 17 | return ( 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default BannerEditButton; 24 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/BannerTag/BannerTag.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | import { ComponentPropsWithoutRef } from 'react'; 5 | 6 | interface TagProps extends ComponentPropsWithoutRef<'div'> { 7 | color: string; 8 | } 9 | 10 | const BannerTag = ({ color, children, ...props }: TagProps) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default BannerTag; 19 | 20 | const StTag = styled.div<{ color: string }>` 21 | display: flex; 22 | 23 | width: fit-content; 24 | 25 | padding: 0.3rem 0.9rem; 26 | 27 | align-items: center; 28 | justify-content: center; 29 | gap: 1rem; 30 | 31 | border-radius: 10rem; 32 | 33 | ${fontsObject.LABEL_3_14_SB} 34 | 35 | color: ${colors.white}; 36 | background-color: ${({ color }) => color}; 37 | 38 | white-space: nowrap; 39 | `; 40 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/ContentTypeField.tsx: -------------------------------------------------------------------------------- 1 | import { Radio } from '@sopt-makers/ui'; 2 | import { useFormContext } from 'react-hook-form'; 3 | 4 | import { 5 | StContentWrapper, 6 | StInputLabel, 7 | StRadioGroup, 8 | } from '@/components/bannerAdmin/CreateBannerModal'; 9 | import FormController from '@/components/bannerAdmin/form/FormController'; 10 | import { CONTENT_KEY, contentList } from '@/components/bannerAdmin/types/form'; 11 | import RequiredIcon from '@/components/org/OrgAdmin/assets/RequiredIcon'; 12 | 13 | const ContentTypeField = () => { 14 | const { watch } = useFormContext(); 15 | 16 | const content = watch('contentType'); 17 | 18 | return ( 19 | 20 | 21 | 콘텐츠 유형 22 | 23 | 24 | 25 | {CONTENT_KEY.map((contentItem, index) => ( 26 | ( 30 | 37 | )} 38 | /> 39 | ))} 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default ContentTypeField; 46 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/CountTag/CountTag.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@sopt-makers/colors'; 4 | import { fontsObject } from '@sopt-makers/fonts'; 5 | 6 | interface CountTagProps { 7 | status: BANNER_STATUS; 8 | children: string | number; 9 | } 10 | 11 | const CountTag = ({ status, children }: CountTagProps) => { 12 | return {children}; 13 | }; 14 | 15 | export default CountTag; 16 | 17 | const TagWrapper = styled.div<{ status: BANNER_STATUS }>` 18 | padding: 0.3rem 0.9rem; 19 | 20 | border-radius: 100px; 21 | 22 | ${fontsObject.LABEL_3_14_SB}; 23 | 24 | ${({ status }) => { 25 | if (status === 'all' || status === 'done') { 26 | return css` 27 | color: ${colors.gray10}; 28 | background-color: ${colors.gray700}; 29 | `; 30 | } 31 | if (status === 'reserved') { 32 | return css` 33 | color: ${colors.secondary}; 34 | background-color: ${colors.orangeAlpha200}; 35 | `; 36 | } 37 | if (status === 'in_progress') { 38 | return css` 39 | color: ${colors.success}; 40 | background-color: ${colors.blueAlpha200}; 41 | `; 42 | } 43 | }} 44 | `; 45 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/DeleteBannerButton.tsx: -------------------------------------------------------------------------------- 1 | import { DialogOptionType, useDialog, useToast } from '@sopt-makers/ui'; 2 | import { useQueryClient } from 'react-query'; 3 | 4 | import { IcTrash } from '@/assets/icons'; 5 | import { useDeleteBanner } from '@/services/api/banner/query'; 6 | 7 | interface DeleteBannerButtonProps { 8 | bannerId: number; 9 | } 10 | 11 | const DeleteBannerButton = ({ bannerId }: DeleteBannerButtonProps) => { 12 | const { mutate: deleteBannerMutate } = useDeleteBanner(); 13 | 14 | const queryClient = useQueryClient(); 15 | 16 | const handleBannerDelete = () => { 17 | deleteBannerMutate(bannerId, { 18 | onSuccess: () => { 19 | queryClient.invalidateQueries('bannerList'); 20 | openToast({ icon: 'success', content: '배너가 삭제되었어요.' }); 21 | }, 22 | onError: () => { 23 | openToast({ icon: 'error', content: '배너 삭제에 실패했어요.' }); 24 | }, 25 | }); 26 | }; 27 | 28 | const { open: openToast } = useToast(); 29 | const { open: openDialog } = useDialog(); 30 | 31 | const option: DialogOptionType = { 32 | title: '배너를 삭제하실 건가요?', 33 | description: '삭제된 배너는 복구가 불가능해요.', 34 | type: 'danger', 35 | typeOptions: { 36 | cancelButtonText: '취소하기', 37 | approveButtonText: '삭제하기', 38 | buttonFunction: handleBannerDelete, 39 | }, 40 | }; 41 | 42 | return ( 43 | 46 | ); 47 | }; 48 | 49 | export default DeleteBannerButton; 50 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | 5 | import { HEADER_LIST } from '@/constants'; 6 | 7 | const Header = () => { 8 | return ( 9 | 10 | {HEADER_LIST.map((title) => ( 11 |

{title}

12 | ))} 13 |
14 | ); 15 | }; 16 | 17 | export default Header; 18 | 19 | const StHeader = styled.header` 20 | display: grid; 21 | grid-template-columns: 1fr 1.2fr 1fr 1fr 1fr 1.2fr 0.8fr; 22 | 23 | padding: 1rem 0; 24 | 25 | align-items: center; 26 | justify-content: space-between; 27 | 28 | text-align: center; 29 | 30 | border-top: 1px solid ${colors.gray700}; 31 | border-bottom: 1px solid ${colors.gray700}; 32 | 33 | & > h3 { 34 | text-align: center; 35 | 36 | ${fontsObject.BODY_3_14_M}; 37 | color: ${colors.gray100}; 38 | 39 | white-space: nowrap; 40 | } 41 | 42 | & > h3:nth-of-type(1) { 43 | margin-left: 3.9rem; 44 | text-align: left; 45 | } 46 | 47 | & > h3:nth-of-type(6) { 48 | margin-left: 5rem; 49 | text-align: left; 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/LinkField.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext } from 'react-hook-form'; 2 | 3 | import { 4 | CustomTextField, 5 | StDescription, 6 | StDescriptionWrapper, 7 | StInputLabel, 8 | } from '@/components/bannerAdmin/CreateBannerModal'; 9 | import Input from '@/components/common/Input'; 10 | 11 | const LinkField = () => { 12 | const { 13 | register, 14 | formState: { errors }, 15 | } = useFormContext(); 16 | 17 | return ( 18 |
19 | 26 | 27 | 28 | {errors.link?.message as string} 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default LinkField; 36 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/PublisherField.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext } from 'react-hook-form'; 2 | 3 | import { 4 | CustomTextField, 5 | StDescription, 6 | StDescriptionWrapper, 7 | } from '@/components/bannerAdmin/CreateBannerModal'; 8 | 9 | const MAX_PUBLISHER_LENGTH = 30; 10 | 11 | const PublisherField = () => { 12 | const { 13 | register, 14 | formState: { errors }, 15 | watch, 16 | } = useFormContext(); 17 | 18 | return ( 19 |
20 | 28 | 29 | 30 | 31 | {errors.publisher?.message as string} 32 | 33 | {`${watch('publisher').length}/${MAX_PUBLISHER_LENGTH}`} 37 | 38 |
39 | ); 40 | }; 41 | 42 | export default PublisherField; 43 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/form/ErrorMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | 4 | export const ErrorMessage = styled.span` 5 | display: inline-block; 6 | font-size: 12px; 7 | font-weight: 500; 8 | line-height: 100%; 9 | 10 | color: ${colors.error}; 11 | `; 12 | 13 | export default ErrorMessage; 14 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/form/FormController/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Controller, ControllerProps, useFormContext } from 'react-hook-form'; 3 | 4 | interface Option { 5 | label: string; 6 | /** 7 | * null 은 placeholder 8 | */ 9 | value: string | null; 10 | /** 11 | * multiple 셀렉트에서 선택된 옵션을 표기할 순서 12 | */ 13 | order?: number; 14 | } 15 | 16 | interface FormControllerProps { 17 | name: string; 18 | render: ControllerProps['render']; 19 | defaultValue?: boolean | string | number | Option | Option[] | string[]; 20 | } 21 | 22 | function FormController({ name, render, defaultValue }: FormControllerProps) { 23 | const { control, formState, setValue } = useFormContext(); 24 | 25 | useEffect(() => { 26 | if (defaultValue === false) { 27 | setValue(name, false); 28 | } 29 | }, [defaultValue, name, setValue]); 30 | 31 | return ( 32 | 37 | render({ field, fieldState, formState }) 38 | } 39 | /> 40 | ); 41 | } 42 | 43 | export default FormController; 44 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/form/HelpMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | import { PropsWithChildren } from 'react'; 5 | 6 | const HelpMessage = ({ children }: PropsWithChildren) => { 7 | return {children}; 8 | }; 9 | 10 | export default HelpMessage; 11 | 12 | export const SHelpMessage = styled.span` 13 | margin-bottom: 8px; 14 | display: inline-block; 15 | ${fontsObject.LABEL_4_12_SB}; 16 | color: ${colors.gray300}; 17 | `; 18 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/types/api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CONTENT_VALUE, 3 | LOCATION_VALUE, 4 | } from '@/components/bannerAdmin/types/form'; 5 | 6 | export interface BannerDetailRequest { 7 | publisher: string; 8 | content_type: (typeof CONTENT_VALUE)[number]; 9 | location: (typeof LOCATION_VALUE)[number]; 10 | start_date: string; 11 | end_date: string; 12 | link?: string; 13 | image_pc: File; 14 | image_mobile: File; 15 | } 16 | 17 | export interface BannerDetailResponse { 18 | success: boolean; 19 | message: string; 20 | data: { 21 | id: number; 22 | status: string; 23 | publisher: string; 24 | content_type: (typeof CONTENT_VALUE)[number]; 25 | location: (typeof LOCATION_VALUE)[number]; 26 | start_date: string; 27 | end_date: string; 28 | link: string; 29 | image_url_pc: string; 30 | image_url_mobile: string; 31 | }; 32 | } 33 | 34 | export interface BannerListResponse { 35 | success: boolean; 36 | message: string; 37 | data: { 38 | data: Banner[]; 39 | totalCount: number; 40 | totalPage: number; 41 | currentPage: number; 42 | hasNextPage: boolean; 43 | hasPrevPage: boolean; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/utils/converUrlToFile.ts: -------------------------------------------------------------------------------- 1 | export const convertUrlToFile = async (url: string) => { 2 | const response = await fetch(url); 3 | const data = await response.blob(); 4 | const ext = url.split('.').pop(); 5 | const filename = url.split('/').pop(); 6 | const metadata = { type: `image/${ext}` }; 7 | 8 | return new File([data], filename!, metadata); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/utils/getBannerStatus.ts: -------------------------------------------------------------------------------- 1 | export const getBannerStatus = (bannerStatus: BANNER_STATUS) => { 2 | if (bannerStatus === 'reserved') return '진행 예정'; 3 | if (bannerStatus === 'in_progress') return '진행중'; 4 | if (bannerStatus === 'done') return '진행 완료'; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/utils/getBannerType.ts: -------------------------------------------------------------------------------- 1 | export const getBannerType = (bannerStatus: BANNER_STATUS) => { 2 | if (bannerStatus === 'reserved') return 'primary'; 3 | if (bannerStatus === 'in_progress') return 'secondary'; 4 | if (bannerStatus === 'done') return 'default'; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/bannerAdmin/utils/getImageSize.ts: -------------------------------------------------------------------------------- 1 | interface ImgSize { 2 | width: number; 3 | height: number; 4 | } 5 | 6 | export const getImageSize = async (url: string): Promise => { 7 | return new Promise((res) => { 8 | const img = new Image(); 9 | 10 | img.src = url; 11 | 12 | img.onload = () => { 13 | const { width, height } = img; 14 | 15 | const size: ImgSize = { width, height }; 16 | 17 | res(size); 18 | }; 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/common/AttendanceChip/index.tsx: -------------------------------------------------------------------------------- 1 | import { StAttendanceChip } from './style'; 2 | 3 | interface Props { 4 | text: string; 5 | } 6 | 7 | function AttendanceChip(props: Props) { 8 | const { text } = props; 9 | 10 | return {text}; 11 | } 12 | 13 | export default AttendanceChip; 14 | -------------------------------------------------------------------------------- /src/components/common/AttendanceChip/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@sopt-makers/colors'; 4 | import { fontsObject } from '@sopt-makers/fonts'; 5 | 6 | export const IndicatorStructure = styled.span` 7 | ${fontsObject.BODY_3_14_M} 8 | display: inline-block; 9 | color: ${colors.gray10}; 10 | border-radius: 8px; 11 | padding: 2px 11px; 12 | margin-right: 6px; 13 | `; 14 | 15 | export const StAttendanceChip = styled(IndicatorStructure)<{ text: string }>` 16 | ${({ text }) => getChipColor(text)} 17 | `; 18 | 19 | const getChipColor = (text: string) => { 20 | switch (text) { 21 | case '출석': 22 | case '참여': 23 | return css` 24 | background-color: ${colors.green900}; 25 | color: ${colors.information}; 26 | `; 27 | case '결석': 28 | return css` 29 | background-color: ${colors.red800}; 30 | color: ${colors.red300}; 31 | `; 32 | case '지각': 33 | return css` 34 | background-color: ${colors.yellow900}; 35 | color: ${colors.attention}; 36 | `; 37 | case '미참여': 38 | return css` 39 | background-color: ${colors.gray600}; 40 | color: ${colors.gray200}; 41 | `; 42 | default: 43 | if (text.includes('-')) { 44 | return css` 45 | background-color: ${colors.red800}; 46 | color: ${colors.red300}; 47 | `; 48 | } 49 | return css` 50 | background-color: ${colors.gray600}; 51 | color: ${colors.gray10}; 52 | padding: 2px 8px; 53 | `; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/common/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { StButton } from './style'; 2 | 3 | export interface Props { 4 | type: 'button' | 'submit'; 5 | text: string; 6 | onClick?: () => void; 7 | disabled?: boolean; 8 | } 9 | 10 | function Button(props: Props) { 11 | const { type, text, onClick, disabled = false } = props; 12 | 13 | return ( 14 | !disabled && onClick && onClick()} 17 | disabled={disabled}> 18 | {text} 19 | 20 | ); 21 | } 22 | 23 | export default Button; 24 | -------------------------------------------------------------------------------- /src/components/common/Button/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@sopt-makers/colors'; 4 | 5 | import { Props } from './index'; 6 | 7 | export const StButton = styled.button>` 8 | height: 4.8rem; 9 | padding: 1.6rem 2.4rem; 10 | font-size: 1.6rem; 11 | line-height: 1; 12 | font-weight: 600; 13 | padding: 1.6rem 2.4rem; 14 | border-radius: 1rem; 15 | &:disabled { 16 | background-color: ${colors.gray600}; 17 | color: ${colors.gray400}; 18 | cursor: default; 19 | } 20 | &:hover { 21 | background-color: ${colors.gray600}; 22 | } 23 | ${({ theme, type }) => 24 | type === 'button' 25 | ? css` 26 | background: none; 27 | color: ${colors.gray200}; 28 | ` 29 | : type === 'submit' 30 | ? css` 31 | background-color: ${colors.white}; 32 | color: ${colors.black}; 33 | ` 34 | : css` 35 | background: none; 36 | color: ${colors.white}; 37 | `}; 38 | `; 39 | -------------------------------------------------------------------------------- /src/components/common/Chip/index.tsx: -------------------------------------------------------------------------------- 1 | import { StChip } from './style'; 2 | 3 | interface Props { 4 | text: string; 5 | } 6 | 7 | function Chip(props: Props) { 8 | const { text } = props; 9 | 10 | return {text}; 11 | } 12 | 13 | export default Chip; 14 | -------------------------------------------------------------------------------- /src/components/common/Chip/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@sopt-makers/colors'; 4 | import { fontsObject } from '@sopt-makers/fonts'; 5 | 6 | export const IndicatorStructure = styled.span` 7 | ${fontsObject.LABEL_4_12_SB} 8 | display: inline-block; 9 | color: ${colors.gray200}; 10 | border: 1px solid ${colors.gray500}; 11 | border-radius: 20px; 12 | padding: 3.5px 8px; 13 | margin-right: 10px; 14 | `; 15 | 16 | export const StSessionIndicator = styled(IndicatorStructure)<{ 17 | attributeName: string; 18 | }>` 19 | ${({ attributeName }) => 20 | attributeName === '세미나' 21 | ? css` 22 | border-color: ${colors.orange600}; 23 | color: ${colors.orange600}; 24 | ` 25 | : attributeName === '행사' 26 | ? css` 27 | border-color: ${colors.blue400}; 28 | color: ${colors.blue400}; 29 | ` 30 | : css` 31 | border-color: ${colors.yellow700}; 32 | color: ${colors.yellow700}; 33 | `} 34 | `; 35 | 36 | export const StChip = styled(IndicatorStructure)<{ text: string }>` 37 | ${({ text }) => getChipColor(text)} 38 | `; 39 | 40 | const getChipColor = (text: string) => { 41 | switch (text) { 42 | case '세미나': 43 | case '공지': 44 | return css` 45 | border-color: ${colors.orange600}; 46 | color: ${colors.orange600}; 47 | `; 48 | case '행사': 49 | case '소식': 50 | return css` 51 | border-color: ${colors.blue400}; 52 | color: ${colors.blue400}; 53 | `; 54 | case '기타': 55 | return css` 56 | border-color: ${colors.yellow700}; 57 | color: ${colors.yellow700}; 58 | `; 59 | default: 60 | return css` 61 | border-color: ${colors.gray500}; 62 | color: ${colors.gray200}; 63 | `; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/common/DropDown/index.tsx: -------------------------------------------------------------------------------- 1 | import { DropdownWrapper } from './style'; 2 | 3 | export interface Props { 4 | type: 'select' | 'times'; 5 | list: string[]; 6 | onItemSelected?: (value: string) => void; 7 | } 8 | 9 | function DropDown(props: Props) { 10 | const { type, list, onItemSelected } = props; 11 | 12 | function handleClick(item: string) { 13 | if (onItemSelected) { 14 | onItemSelected(item); 15 | } 16 | } 17 | 18 | return ( 19 | 20 |
21 | {list.map((list) => ( 22 |

handleClick(list)}> 23 | {list} 24 |

25 | ))} 26 |
27 |
28 | ); 29 | } 30 | 31 | export default DropDown; 32 | -------------------------------------------------------------------------------- /src/components/common/DropDown/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@sopt-makers/colors'; 4 | 5 | import zIndex from '@/utils/zIndex'; 6 | 7 | import { Props } from './index'; 8 | 9 | export const DropdownWrapper = styled.div>` 10 | position: absolute; 11 | 12 | top: 100%; 13 | 14 | width: 100%; 15 | min-width: 11rem; 16 | height: auto; 17 | margin-top: 1rem; 18 | padding: 0.8rem 0.7rem; 19 | 20 | box-shadow: 0px 5px 20px 0px rgba(63, 64, 66, 0.15); 21 | border-radius: 1.3rem; 22 | 23 | background-color: ${colors.gray500}; 24 | 25 | z-index: ${zIndex.select}; 26 | 27 | animation: appearDropdown 0.6s; 28 | & > div { 29 | display: flex; 30 | flex-direction: column; 31 | gap: 0.4rem; 32 | 33 | max-height: auto; 34 | 35 | ${({ type }) => 36 | type === 'times' && 37 | css` 38 | max-height: 20.2rem; 39 | overflow-y: scroll; // 세로 스크롤만 허용 40 | overflow-x: hidden; // 가로 스크롤 숨기기 41 | 42 | ::-webkit-scrollbar { 43 | width: 0.6rem; 44 | } 45 | 46 | ::-webkit-scrollbar-thumb { 47 | background-color: ${colors.gray300}; 48 | border-radius: 0.8rem; 49 | } 50 | `} 51 | 52 | & > p { 53 | padding: 0.5rem 0.9rem; 54 | 55 | color: ${colors.gray10}; 56 | font-size: 1.6rem; 57 | font-style: normal; 58 | font-weight: 500; 59 | line-height: 100%; /* 1.6rem */ 60 | letter-spacing: -0.016rem; 61 | 62 | border-radius: 0.6rem; 63 | 64 | &:hover { 65 | background-color: ${colors.gray400}; 66 | 67 | cursor: pointer; 68 | } 69 | } 70 | } 71 | 72 | @keyframes appearDropdown { 73 | from { 74 | opacity: 0; 75 | transform: translateY(-1rem); 76 | } 77 | to { 78 | opacity: 1; 79 | transform: translateY(0rem); 80 | } 81 | } 82 | `; 83 | -------------------------------------------------------------------------------- /src/components/common/FilterButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FilterButtonItem, 3 | FilterWrapper, 4 | StUnderline, 5 | } from '@/components/common/FilterButton/style'; 6 | 7 | interface Props { 8 | list: T[]; 9 | translator?: Record; 10 | onChange: (item: T) => void; 11 | selected: T; 12 | } 13 | 14 | function FilterButton(props: Props) { 15 | const { list, translator, selected, onChange } = props; 16 | 17 | return ( 18 | <> 19 | 20 | {list.map((item: T) => ( 21 | onChange(item)}> 25 | {translator ? translator[item] : item} 26 | 27 | ))} 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default FilterButton; 35 | -------------------------------------------------------------------------------- /src/components/common/FilterButton/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@sopt-makers/colors'; 4 | import { fontsObject } from '@sopt-makers/fonts'; 5 | 6 | export const FilterWrapper = styled.div` 7 | display: flex; 8 | gap: 26px; 9 | `; 10 | export const StUnderline = styled.div` 11 | width: calc(100vw - 212px); 12 | margin-left: calc((100vw - 212px - 100%) / -2); 13 | height: 1px; 14 | background-color: ${colors.gray800}; 15 | `; 16 | export const FilterButtonItem = styled.button<{ selected: boolean }>` 17 | ${fontsObject.TITLE_4_20_SB} 18 | 19 | padding-bottom: 13px; 20 | transition: all 0.2s; 21 | 22 | ${({ selected }) => 23 | selected 24 | ? css` 25 | color: ${colors.gray30}; 26 | border-bottom: 3px solid ${colors.gray30}; 27 | ` 28 | : css` 29 | color: ${colors.gray400}; 30 | border-bottom: 3px solid transparent; 31 | `} 32 | 33 | &:hover { 34 | color: ${colors.gray100}; 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /src/components/common/FloatingButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { StFloatingButton } from './style'; 4 | 5 | interface Props { 6 | content: ReactNode; 7 | onClick: () => void; 8 | } 9 | 10 | function FloatingButton(props: Props) { 11 | const { content, onClick } = props; 12 | 13 | return {content}; 14 | } 15 | 16 | export default FloatingButton; 17 | -------------------------------------------------------------------------------- /src/components/common/FloatingButton/style.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | 5 | import zIndex from '@/utils/zIndex'; 6 | 7 | export const StFloatingButton = styled.button` 8 | position: fixed; 9 | right: 60px; 10 | bottom: 60px; 11 | padding: 14px 30px; 12 | border-radius: 60px; 13 | z-index: ${zIndex.footer}; 14 | background-color: ${colors.gray10}; 15 | color: ${colors.gray900}; 16 | ${fontsObject.TITLE_4_20_SB} 17 | `; 18 | -------------------------------------------------------------------------------- /src/components/common/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { StFooter, StFooterWrap } from './style'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | } 8 | 9 | function Footer(props: Props) { 10 | const { children } = props; 11 | 12 | return ( 13 | 14 | 15 |
{children}
16 |
17 |
18 | ); 19 | } 20 | 21 | export default Footer; 22 | -------------------------------------------------------------------------------- /src/components/common/Footer/style.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | import zIndex from '@/utils/zIndex'; 4 | 5 | export const StFooterWrap = styled.div` 6 | width: 100%; 7 | height: 15rem; 8 | `; 9 | export const StFooter = styled.footer` 10 | position: fixed; 11 | z-index: ${zIndex.footer}; 12 | bottom: 0; 13 | left: 22rem; 14 | width: calc(100% - 22rem); 15 | height: 11rem; 16 | background-color: ${({ theme }) => theme.color.grayscale.gray20}; 17 | box-shadow: 6px 0 40px 0 rgba(0, 0, 0, 0.06); 18 | & > div { 19 | width: 100%; 20 | height: 100%; 21 | max-width: 98rem; 22 | margin: 0 auto; 23 | padding: 2rem 0; 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/components/common/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { StFormLayout } from './style'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | } 8 | 9 | const Form = (props: Props) => { 10 | const { children } = props; 11 | 12 | return {children}; 13 | }; 14 | 15 | export default Form; 16 | -------------------------------------------------------------------------------- /src/components/common/Form/style.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | 4 | export const StFormLayout = styled.div<{ hasValue?: boolean }>` 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | 9 | width: 100%; 10 | height: 4.4rem; 11 | 12 | border: ${({ hasValue, theme }) => 13 | hasValue 14 | ? `1px solid ${theme.color.grayscale.black40}` 15 | : `1px solid ${colors.gray30}`}; 16 | box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); 17 | border-radius: 8px; 18 | `; 19 | -------------------------------------------------------------------------------- /src/components/common/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | 3 | import AdminStatusDevtools from '@/components/devTools/AdminStatus'; 4 | 5 | import { StHeader } from './style'; 6 | 7 | function Header() { 8 | const router = useRouter(); 9 | 10 | const logout = () => { 11 | sessionStorage.clear(); 12 | router.replace('/'); 13 | }; 14 | 15 | return ( 16 | 17 | {process.env.NEXT_PUBLIC_API_URL !== 'PRODUCTION' && ( 18 |
19 | 20 |
21 | )} 22 | 23 | 26 |
27 | ); 28 | } 29 | 30 | export default Header; 31 | -------------------------------------------------------------------------------- /src/components/common/Header/style.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | 4 | import zIndex from '@/utils/zIndex'; 5 | 6 | export const StHeader = styled.header` 7 | display: flex; 8 | justify-content: flex-end; 9 | align-items: center; 10 | 11 | position: fixed; 12 | z-index: ${zIndex.header}; 13 | background-color: ${colors.background}; 14 | top: 0; 15 | left: 22rem; 16 | width: calc(100% - 22rem); 17 | height: 8rem; 18 | padding: 0 2.6rem; 19 | 20 | div.status_devtools { 21 | width: fit-content; 22 | padding: 2rem; 23 | } 24 | 25 | button { 26 | width: 10.2rem; 27 | padding: 0.3rem 0.6rem; 28 | 29 | color: ${colors.gray200}; 30 | background-color: ${colors.gray800}; 31 | 32 | border-radius: 1.9rem; 33 | 34 | cursor: pointer; 35 | 36 | &:hover { 37 | color: ${colors.gray10}; 38 | background-color: ${colors.gray700}; 39 | } 40 | &:active { 41 | background-color: ${colors.gray600}; 42 | } 43 | 44 | & > p { 45 | text-align: center; 46 | font-family: SUIT; 47 | font-size: 1.6rem; 48 | font-style: normal; 49 | font-weight: 600; 50 | line-height: 150%; /* 2.4rem */ 51 | letter-spacing: -0.024rem; 52 | } 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /src/components/common/HelperText/index.tsx: -------------------------------------------------------------------------------- 1 | import { StContent, StTextBox } from './style'; 2 | 3 | interface Props { 4 | text: string; 5 | StWrapper: typeof StTextBox; 6 | } 7 | 8 | function HelperText(props: Props) { 9 | const { text, StWrapper } = props; 10 | 11 | return ( 12 | 13 | 14 | 15 |

{text}

16 |
17 | 18 |
19 |
20 |
21 | ); 22 | } 23 | 24 | export default HelperText; 25 | -------------------------------------------------------------------------------- /src/components/common/HelperText/style.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const StTextBox = styled.div` 4 | background-color: ${({ theme }) => theme.color.main.newBlue}; 5 | padding: 15px 20px; 6 | border-radius: 10px; 7 | 8 | p { 9 | font-size: 14px; 10 | font-weight: 500; 11 | line-height: 140%; 12 | white-space: pre-line; 13 | color: ${({ theme }) => theme.color.grayscale.white100}; 14 | } 15 | `; 16 | 17 | export const StContent = styled.div` 18 | width: max-content; 19 | animation: popup 0.6s ease-out; 20 | 21 | .triangle { 22 | width: 0px; 23 | height: 0px; 24 | border-top: 12px solid ${({ theme }) => theme.color.main.newBlue}; 25 | border-left: 6px solid transparent; 26 | border-right: 6px solid transparent; 27 | margin-left: 76%; 28 | } 29 | 30 | @keyframes popup { 31 | 0% { 32 | transform: translateY(5%) scale(90%); 33 | } 34 | 15% { 35 | transform: translateY(-10%) scale(95%); 36 | } 37 | 50% { 38 | transform: translateY(10%) scale(100%); 39 | } 40 | 100% { 41 | transform: translateY(0%) scale(100%); 42 | } 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /src/components/common/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, ChangeEventHandler } from 'react'; 2 | 3 | import { StInput } from './style'; 4 | 5 | interface Props { 6 | type: string; 7 | placeholder?: string; 8 | value?: string; 9 | readOnly?: boolean; 10 | onChange?: (event: ChangeEvent) => void; 11 | } 12 | 13 | function Input(props: Props) { 14 | const { type, placeholder, value, readOnly = false, onChange } = props; 15 | 16 | return ( 17 | 24 | ); 25 | } 26 | 27 | export default Input; 28 | -------------------------------------------------------------------------------- /src/components/common/Input/style.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors } from '@sopt-makers/colors'; 3 | import { fontsObject } from '@sopt-makers/fonts'; 4 | 5 | export const StInput = styled.input` 6 | padding: 1rem 1.4rem; 7 | 8 | ${fontsObject.LABEL_1_18_SB} 9 | 10 | color: ${colors.gray10}; 11 | background-color: ${colors.gray700}; 12 | border: none; 13 | outline: none; 14 | 15 | border-radius: 0.8rem; 16 | 17 | &::placeholder { 18 | color: ${colors.gray400}; 19 | } 20 | 21 | &:not(:read-only):focus { 22 | background-color: ${colors.gray600}; 23 | outline: 0.1rem solid ${colors.gray300}; 24 | } 25 | &:read-only { 26 | cursor: default; 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/components/common/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { ReactNode } from 'react'; 3 | 4 | import Header from '@/components/common/Header'; 5 | import Nav from '@/components/common/Nav'; 6 | 7 | import { StLayout } from './style'; 8 | 9 | interface Props { 10 | children: ReactNode; 11 | } 12 | 13 | function Layout(props: Props) { 14 | const { children } = props; 15 | 16 | const router = useRouter(); 17 | 18 | if (router.pathname === '/') return <>{children}; 19 | return ( 20 | 21 |