├── .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 |
--------------------------------------------------------------------------------
/src/assets/icons/IcAttendanceMenu.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/IcBannerMenu.svg:
--------------------------------------------------------------------------------
1 |
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 |
35 | );
36 | }
37 |
38 | export default IcCheckBox;
39 |
--------------------------------------------------------------------------------
/src/assets/icons/IcDate.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/IcDeleteFile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/IcDropdownCheck.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/IcEdit.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/IcGoPrev.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/IcModalClose.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/IcMore.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/assets/icons/IcNewDropdown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/IcOrgMenu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/IcPaginationLeft.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/icons/IcPaginationRight.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/icons/IcPlace.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/IcTrash.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/icons/IcUpload.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
25 |
26 | );
27 | }
28 |
29 | export default SessionListFooter;
30 |
--------------------------------------------------------------------------------
/src/components/attendanceAdmin/session/SessionListFooter/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const StFooterWrapper = styled.div`
4 | display: flex;
5 | justify-content: flex-end;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/components/attendanceAdmin/totalScore/MemberList/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 StPageHeader = styled.header`
6 | h1 {
7 | ${fontsObject.TITLE_1_32_SB}
8 | color: ${colors.gray10};
9 | margin-bottom: 41px;
10 | }
11 | p {
12 | ${fontsObject.TITLE_6_16_SB}
13 | color: ${colors.gray200};
14 | margin: 56px 0 18px 12px;
15 | }
16 | `;
17 |
18 | export const StListItem = styled.li`
19 | color: ${colors.gray100};
20 | display: flex;
21 | justify-content: space-between;
22 | padding: 18px 43px 18px 33px;
23 |
24 | .member-info-wrap {
25 | display: flex;
26 |
27 | .index {
28 | ${fontsObject.BODY_3_14_M}
29 | width: 26px;
30 | margin-right: 33px;
31 | }
32 | .member-info > div:first-of-type {
33 | display: flex;
34 | align-items: center;
35 | gap: 15px;
36 | }
37 | .member-name {
38 | ${fontsObject.TITLE_5_18_SB}
39 | color: ${colors.gray30};
40 | margin-bottom: 4px;
41 | }
42 | .member-university {
43 | ${fontsObject.BODY_3_14_M}
44 | color: ${colors.gray400};
45 | }
46 | }
47 | .member-score-wrap {
48 | display: flex;
49 | align-items: center;
50 |
51 | .attendance {
52 | ${fontsObject.BODY_2_16_M}
53 | color: ${colors.gray100};
54 | margin-right: 38px;
55 |
56 | span {
57 | color: ${colors.gray500};
58 | margin-right: 10px;
59 | }
60 | }
61 | .member-score {
62 | ${fontsObject.BODY_2_16_M}
63 | color: ${colors.gray50};
64 | background-color: ${colors.gray700};
65 | padding: 5px 13px;
66 | border-radius: 30px;
67 | width: fit-content;
68 | margin: 0 auto;
69 | }
70 | .minus-score {
71 | color: ${colors.error};
72 | }
73 | }
74 | `;
75 |
--------------------------------------------------------------------------------
/src/components/bannerAdmin/BannerEditButton.tsx:
--------------------------------------------------------------------------------
1 | import { IcEdit } from '@/assets/icons';
2 |
3 | interface BannerEditButtonProps {
4 | onEditModalOpen: (bannerId: number) => void;
5 | bannerId: number;
6 | }
7 |
8 | const BannerEditButton = ({
9 | onEditModalOpen,
10 | bannerId,
11 | }: BannerEditButtonProps) => {
12 | const handleEditClick = () => {
13 | onEditModalOpen(bannerId);
14 | };
15 |
16 | return (
17 |
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 |
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 |
22 |
23 |
24 | {children}
25 |
26 |
27 | );
28 | }
29 |
30 | export default Layout;
31 |
--------------------------------------------------------------------------------
/src/components/common/Layout/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const StLayout = styled.div`
4 | width: 100%;
5 | display: flex;
6 | .main-wrapper {
7 | width: 100%;
8 | padding: 88px 0 0 212px;
9 | & > main {
10 | width: 100%;
11 |
12 | padding: 0px 49px 0px 124px;
13 | }
14 | }
15 | `;
16 |
--------------------------------------------------------------------------------
/src/components/common/ListActionButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { StButton } from './style';
2 |
3 | interface Props {
4 | text: string;
5 | onClick?: (e: React.MouseEvent) => void;
6 | disabled?: boolean;
7 | }
8 |
9 | function ListActionButton(props: Props) {
10 | const { text, onClick, disabled = false } = props;
11 |
12 | return (
13 | {
15 | if (!disabled && onClick) {
16 | e.stopPropagation();
17 | onClick(e);
18 | }
19 | }}
20 | disabled={disabled}>
21 | {text}
22 |
23 | );
24 | }
25 |
26 | export default ListActionButton;
27 |
--------------------------------------------------------------------------------
/src/components/common/ListActionButton/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 StButton = styled.button`
6 | ${fontsObject.BODY_3_14_M}
7 | color: ${colors.gray10};
8 | transition: transform 0.1s;
9 |
10 | display: inline-block;
11 |
12 | padding: 6px 10px;
13 | border: 1px solid ${colors.gray300};
14 | border-radius: 8px;
15 | background-color: ${colors.gray600};
16 | cursor: pointer;
17 |
18 | &:disabled {
19 | color: ${colors.gray500};
20 | background-color: ${colors.gray800};
21 | border-color: ${colors.gray800};
22 | cursor: default;
23 | }
24 | &:not(:disabled):hover {
25 | transform: scale(1.15);
26 | }
27 | `;
28 |
--------------------------------------------------------------------------------
/src/components/common/ListWrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { StList } from './style';
4 |
5 | interface Props {
6 | children: ReactNode;
7 | }
8 |
9 | function ListWrapper(props: Props) {
10 | const { children } = props;
11 |
12 | return {children};
13 | }
14 |
15 | export default ListWrapper;
16 |
--------------------------------------------------------------------------------
/src/components/common/ListWrapper/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 |
4 | export const StList = styled.ul`
5 | width: 100%;
6 | display: flex;
7 | flex-direction: column;
8 | gap: 10px;
9 | margin-bottom: 120px;
10 |
11 | & > li {
12 | border-radius: 10px;
13 | border: 1px solid ${colors.gray800};
14 | color: ${colors.gray300};
15 |
16 | &:not(.no-pointer):hover {
17 | border: 1px solid ${colors.gray600};
18 | background-color: ${colors.gray900};
19 | cursor: pointer;
20 | }
21 | &.focused {
22 | border: 1px solid ${colors.gray600};
23 | background-color: ${colors.gray900};
24 |
25 | &:hover {
26 | cursor: default;
27 | }
28 | }
29 | }
30 | `;
31 |
--------------------------------------------------------------------------------
/src/components/common/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | import { StyledLoading } from './style';
4 |
5 | interface Props {
6 | dimmed?: boolean;
7 | full?: boolean;
8 | }
9 |
10 | function Loading(props: Props) {
11 | const { dimmed = true, full = true } = props;
12 |
13 | useEffect(() => {
14 | const body = document.querySelector('body');
15 |
16 | if (body && full) {
17 | body.style.overflow = 'hidden';
18 | }
19 | return () => {
20 | if (body && full) {
21 | body.style.overflow = 'scroll';
22 | }
23 | };
24 | }, [full]);
25 |
26 | return (
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default Loading;
34 |
--------------------------------------------------------------------------------
/src/components/common/Loading/style.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import styled from '@emotion/styled';
3 |
4 | import zIndex from '@/utils/zIndex';
5 |
6 | export const StyledLoading = styled.div<{ dimmed: boolean; full: boolean }>`
7 | background: ${({ dimmed }) =>
8 | dimmed ? 'rgba(0, 0, 0, 0.4)' : 'transparent'};
9 | ${({ full }) =>
10 | full &&
11 | css`
12 | width: 100%;
13 | height: 100vh;
14 | position: fixed;
15 | top: 0;
16 | left: 0;
17 | z-index: ${zIndex.dim};
18 | `}
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 |
23 | & > div {
24 | width: 4.8rem;
25 | height: 4.8rem;
26 | border-radius: 100%;
27 | border: 4px solid white;
28 | border-width: 4px;
29 | border-top-color: ${({ theme }) => theme.color.grayscale.black100};
30 | animation: spin 1s linear infinite;
31 | }
32 | @keyframes spin {
33 | from {
34 | transform: rotate(0deg);
35 | }
36 | to {
37 | transform: rotate(360deg);
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/components/common/Nav/GenerationDropDown/index.tsx:
--------------------------------------------------------------------------------
1 | import { EmotionJSX } from '@emotion/react/types/jsx-namespace';
2 | import { useRouter } from 'next/router';
3 | import { useState } from 'react';
4 |
5 | import { IcNewDropdown } from '@/assets/icons';
6 | import { useRecoilGenerationSSR } from '@/hooks/useRecoilGenerationSSR';
7 | import { GENERATION_INFO } from '@/utils/nav';
8 |
9 | import {
10 | StDropdownGeneration,
11 | StGenerationDropdown,
12 | StSelectedGeneration,
13 | StWrapper,
14 | } from './style';
15 |
16 | function GenerationDropDown() {
17 | const router = useRouter();
18 |
19 | const [currentGeneration, setCurrentGeneration] = useRecoilGenerationSSR();
20 | const [isDropdownOn, setIsDropdownOn] = useState(false);
21 |
22 | const handleSelectedGeneration = (selectedGeneration: string) => {
23 | setCurrentGeneration(selectedGeneration);
24 | setIsDropdownOn(false);
25 | const pathSegments = router.asPath.split('/');
26 | if (pathSegments[pathSegments.length - 1].match(/^\d+$/)) {
27 | router.push('/attendanceAdmin/session');
28 | }
29 | };
30 |
31 | return (
32 |
33 | setIsDropdownOn(!isDropdownOn)}
35 | onBlur={() => setIsDropdownOn(false)}>
36 |
37 |
38 | {currentGeneration}기
39 |
40 |
41 |
42 | {isDropdownOn && (
43 |
44 |
45 | {GENERATION_INFO.map((info) => {
46 | const { generation, slogan } = info;
47 |
48 | return (
49 |
handleSelectedGeneration(generation)}>
52 |
53 |
{generation}기
54 | {slogan} SOPT
55 |
56 |
57 | );
58 | })}
59 |
60 |
61 | )}
62 |
63 | );
64 | }
65 |
66 | export default GenerationDropDown;
67 |
--------------------------------------------------------------------------------
/src/components/common/Nav/GenerationDropDown/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 StWrapper = styled.div`
6 | padding: 0 1.8rem 3.3rem 1.8rem;
7 | `;
8 |
9 | const GenerationContainer = styled.div`
10 | display: flex;
11 | gap: 1.4rem;
12 | padding: 1rem 0 1rem 1.2rem;
13 | border-radius: 1rem;
14 |
15 | cursor: pointer;
16 |
17 | & > div {
18 | display: flex;
19 | flex-direction: column;
20 | gap: 0.1rem;
21 |
22 | & > h1 {
23 | display: flex;
24 | align-items: center;
25 | gap: 0.6rem;
26 |
27 | ${fontsObject.HEADING_7_16_B}
28 | color: ${colors.gray10};
29 | }
30 | & > h2 {
31 | ${fontsObject.LABEL_4_12_SB}
32 | color: ${colors.gray300};
33 | }
34 | }
35 | `;
36 |
37 | export const StSelectedGeneration = styled(GenerationContainer)`
38 | &:hover {
39 | background-color: ${colors.gray800};
40 | }
41 | &:active {
42 | background-color: ${colors.gray700};
43 | }
44 |
45 | & > svg > path > fill {
46 | border-radius: 0.7rem;
47 | }
48 | `;
49 |
50 | export const StDropdownGeneration = styled(GenerationContainer)`
51 | &:hover {
52 | background-color: ${colors.gray600};
53 | }
54 | &:active {
55 | background-color: ${colors.gray500};
56 | }
57 | `;
58 |
59 | export const StGenerationDropdown = styled.div`
60 | position: absolute;
61 |
62 | margin-top: 1rem;
63 |
64 | width: 17.6rem;
65 | height: auto;
66 |
67 | background-color: ${colors.gray700};
68 |
69 | border-radius: 1.3rem;
70 | box-shadow: 0px 5px 20px 0px rgba(63, 64, 66, 0.15);
71 |
72 | animation: appearDropdown 0.6s;
73 |
74 | @keyframes appearDropdown {
75 | from {
76 | opacity: 0;
77 | transform: translateY(-1rem);
78 | }
79 | to {
80 | opacity: 1;
81 | transform: translateY(0rem);
82 | }
83 | }
84 |
85 | & > div {
86 | display: flex;
87 | flex-direction: column;
88 | gap: 0.6rem;
89 |
90 | padding: 0.8rem 0.7rem;
91 | }
92 | `;
93 |
--------------------------------------------------------------------------------
/src/components/common/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { Fragment, useContext } from 'react';
3 |
4 | import { adminStatusContext } from '@/components/devTools/AdminContextProvider';
5 | import { MENU_LIST } from '@/utils/nav';
6 |
7 | import GenerationDropDown from './GenerationDropDown';
8 | import { StMenu, StNavWrapper, StSoptLogo, StSubMenu } from './style';
9 |
10 | function Nav() {
11 | const router = useRouter();
12 | const { status } = useContext(adminStatusContext);
13 |
14 | const filteredMenuList =
15 | status === 'MAKERS'
16 | ? MENU_LIST.filter(
17 | (menu) => menu.title === '알림 관리' || menu.title === '배너 관리',
18 | )
19 | : MENU_LIST;
20 |
21 | const handleSubMenuClick = (path: string) => {
22 | router.push(path);
23 | };
24 |
25 | return (
26 |
27 |
30 |
31 | {filteredMenuList.map((menu) => (
32 |
33 | router.pathname.includes(path))
37 | }
38 | onClick={() => menu.path && handleSubMenuClick(menu.path[0])}>
39 |
40 |
41 | {menu.title}
42 |
43 |
44 | {menu.subMenu &&
45 | menu.subMenu.map((subMenu, i) => (
46 | handleSubMenuClick(menu.path[i])}>
53 | {subMenu}
54 |
55 | ))}
56 |
57 | ))}
58 |
59 | );
60 | }
61 |
62 | export default Nav;
63 |
--------------------------------------------------------------------------------
/src/components/common/OptionTemplate/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { StTemplateWrapper } from './style';
4 |
5 | interface Props {
6 | title: string;
7 | children: ReactNode;
8 | }
9 |
10 | function OptionTemplate(props: Props) {
11 | const { title, children } = props;
12 |
13 | return (
14 |
15 | {title}
16 | {children}
17 |
18 | );
19 | }
20 |
21 | export default OptionTemplate;
22 |
--------------------------------------------------------------------------------
/src/components/common/OptionTemplate/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 StTemplateWrapper = styled.div`
6 | position: relative;
7 | display: flex;
8 | flex-direction: column;
9 | gap: 0.6rem;
10 |
11 | & > p {
12 | margin-top: 1.6rem;
13 |
14 | ${fontsObject.LABEL_3_14_SB}
15 |
16 | color: ${colors.gray300};
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/src/components/common/PartFilter/index.tsx:
--------------------------------------------------------------------------------
1 | import FilterButton from '@/components/common/FilterButton';
2 | import {
3 | allPartTranslator,
4 | partList,
5 | partTranslator,
6 | } from '@/utils/translator';
7 |
8 | interface Props {
9 | selected: PART;
10 | onChangePart: (part: PART) => void;
11 | isAllPart?: boolean;
12 | }
13 |
14 | function PartFilter(props: Props) {
15 | const { selected, onChangePart, isAllPart = false } = props;
16 |
17 | return (
18 |
19 | list={partList}
20 | selected={selected}
21 | onChange={onChangePart}
22 | translator={isAllPart ? allPartTranslator : partTranslator}
23 | />
24 | );
25 | }
26 |
27 | export default PartFilter;
28 |
--------------------------------------------------------------------------------
/src/components/common/Selector/index.tsx:
--------------------------------------------------------------------------------
1 | import { IcNewDropdown } from '@/assets/icons';
2 |
3 | import { StSelectorWrapper } from './style';
4 |
5 | interface Props {
6 | content: string | null;
7 | onClick?: () => void;
8 | isDisabledValue?: boolean;
9 | readOnly?: boolean;
10 | }
11 |
12 | function Selector(props: Props) {
13 | const { content, onClick, isDisabledValue = false, readOnly = false } = props;
14 |
15 | return (
16 |
20 | {content}
21 |
22 |
23 | );
24 | }
25 |
26 | export default Selector;
27 |
--------------------------------------------------------------------------------
/src/components/common/Selector/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 |
4 | export const StSelectorWrapper = styled.div<{
5 | isDisabledValue: boolean;
6 | readOnly: boolean;
7 | }>`
8 | display: flex;
9 | justify-content: space-between;
10 | align-items: center;
11 | gap: 0.7rem;
12 |
13 | min-width: 8.6rem;
14 |
15 | padding: 1rem 1.4rem;
16 |
17 | font-size: 1.8rem;
18 | font-style: normal;
19 | font-weight: 500;
20 | line-height: 100%; /* 1.8rem */
21 | letter-spacing: -0.018rem;
22 |
23 | color: ${({ isDisabledValue }) =>
24 | isDisabledValue ? colors.gray400 : colors.gray10};
25 |
26 | background-color: ${colors.gray700};
27 | border-radius: 0.8rem;
28 |
29 | cursor: ${({ isDisabledValue, readOnly }) =>
30 | isDisabledValue || readOnly ? 'default' : 'pointer'};
31 |
32 | & > svg > path {
33 | fill: ${({ isDisabledValue }) =>
34 | isDisabledValue ? colors.gray400 : colors.gray10};
35 | }
36 | `;
37 |
--------------------------------------------------------------------------------
/src/components/common/icons/IcDropDown.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | width?: number;
3 | height?: number;
4 | color?: string;
5 | direction?: 'LEFT' | 'RIGHT' | 'UP' | 'DOWN';
6 | }
7 |
8 | function IcDropdown(props: Props) {
9 | const {
10 | width = 14,
11 | height = 14,
12 | color = '#3C3D40',
13 | direction = 'DOWN',
14 | } = props;
15 |
16 | const rotate = () => {
17 | switch (direction) {
18 | case 'LEFT':
19 | return 'rotate(90deg)';
20 | case 'RIGHT':
21 | return 'rotate(180deg)';
22 | case 'UP':
23 | return 'rotate(-90deg)';
24 | case 'DOWN':
25 | default:
26 | return 'rotate(0deg)';
27 | }
28 | };
29 |
30 | return (
31 |
43 | );
44 | }
45 |
46 | export default IcDropdown;
47 |
--------------------------------------------------------------------------------
/src/components/common/inputContainer/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { StLayout } from './style';
4 |
5 | interface Props {
6 | title: string;
7 | children?: ReactNode;
8 | onClick?: () => void;
9 | }
10 |
11 | const InputContainer = (props: Props) => {
12 | const { title, children, onClick } = props;
13 |
14 | return (
15 |
16 | {title}
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export default InputContainer;
23 |
--------------------------------------------------------------------------------
/src/components/common/inputContainer/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 |
4 | export const StLayout = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 |
8 | width: 100%;
9 | height: 100%;
10 |
11 | & > p {
12 | padding-bottom: 0.6rem;
13 |
14 | font-weight: 500;
15 | font-size: 14px;
16 | line-height: 20px;
17 | letter-spacing: -0.02em;
18 |
19 | color: ${colors.gray100};
20 | }
21 | & > div {
22 | display: flex;
23 | align-items: center;
24 |
25 | width: 100%;
26 | height: auto;
27 |
28 | box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
29 | border-radius: 8px;
30 |
31 | & > span {
32 | width: 100%;
33 |
34 | padding: 1rem 1.4rem;
35 |
36 | color: ${({ theme }) => theme.color.grayscale.black40};
37 | border: 1px solid ${({ theme }) => theme.color.grayscale.black40};
38 | border-radius: 0.8rem;
39 |
40 | font-weight: 500;
41 | font-size: 1.6rem;
42 | line-height: 2.4rem;
43 | letter-spacing: -0.02em;
44 | }
45 | }
46 | `;
47 |
--------------------------------------------------------------------------------
/src/components/common/modal/ModalFooter/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { StModalFooterWrapper } from './style';
4 |
5 | interface Props {
6 | children: ReactNode;
7 | }
8 |
9 | function ModalFooter(props: Props) {
10 | const { children } = props;
11 | return {children};
12 | }
13 |
14 | export default ModalFooter;
15 |
--------------------------------------------------------------------------------
/src/components/common/modal/ModalFooter/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const StModalFooterWrapper = styled.footer`
4 | display: flex;
5 | align-items: center;
6 |
7 | height: 10.6rem;
8 |
9 | padding: 2.5rem 3.2rem;
10 |
11 | border-radius: 0 0 1.2rem 1.2rem;
12 | `;
13 |
--------------------------------------------------------------------------------
/src/components/common/modal/ModalHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { IcModalClose } from '@/assets/icons';
4 |
5 | import { StModalHeader } from './style';
6 |
7 | interface Props {
8 | title: string;
9 | desc?: string;
10 | tag?: ReactNode;
11 | onClose: () => void;
12 | }
13 |
14 | function ModalHeader(props: Props) {
15 | const { title, desc, tag, onClose } = props;
16 |
17 | return (
18 |
19 |
20 | {tag && tag}
21 |
{title}
22 | {desc}
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default ModalHeader;
30 |
--------------------------------------------------------------------------------
/src/components/common/modal/ModalHeader/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 StModalHeader = styled.header`
6 | display: flex;
7 | justify-content: space-between;
8 | align-items: center;
9 |
10 | padding: 2.2rem 2.8rem 2.2rem 3.2rem;
11 |
12 | & > div.title {
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | gap: 1.6rem;
17 |
18 | & > h1 {
19 | ${fontsObject.HEADING_3_28_B};
20 | color: ${colors.gray10};
21 | }
22 |
23 | & > h2 {
24 | font-size: 1.4rem;
25 | font-style: normal;
26 | font-weight: 300;
27 | line-height: 160%;
28 | letter-spacing: -0.021rem;
29 |
30 | color: ${colors.gray300};
31 | }
32 | }
33 | & > svg {
34 | cursor: pointer;
35 |
36 | &:hover {
37 | fill: ${colors.gray700};
38 | }
39 | &:active {
40 | fill: ${colors.gray600};
41 | }
42 | }
43 | `;
44 |
--------------------------------------------------------------------------------
/src/components/common/modal/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { StModalBackground, StModalWrapper } from './style';
4 |
5 | interface Props {
6 | children: ReactNode;
7 | onClose?: () => void;
8 | }
9 |
10 | function Modal(props: Props) {
11 | const { children, onClose } = props;
12 |
13 | return (
14 |
15 | {children}
16 |
17 | );
18 | }
19 |
20 | export default Modal;
21 |
--------------------------------------------------------------------------------
/src/components/common/modal/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 StModalBackground = styled.div`
7 | position: fixed;
8 | top: 0;
9 | left: 0;
10 | width: 100%;
11 | height: 100%;
12 | background-color: rgba(15, 15, 18, 0.8);
13 | z-index: ${zIndex.dim};
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | `;
18 |
19 | export const StModalWrapper = styled.div`
20 | width: auto;
21 | height: auto;
22 |
23 | background-color: ${colors.gray800};
24 | box-shadow: 0px 0px 40px rgba(16, 24, 40, 0.06);
25 | border-radius: 1.2rem;
26 |
27 | z-index: ${zIndex.modal};
28 |
29 | animation: appearModal 0.6s forwards;
30 |
31 | @keyframes appearModal {
32 | from {
33 | opacity: 0;
34 | transform: translateY(2rem);
35 | }
36 | to {
37 | opacity: 1;
38 | transform: translateY(0);
39 | }
40 | }
41 | `;
42 |
--------------------------------------------------------------------------------
/src/components/devTools/AdminContextProvider/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | Dispatch,
4 | ReactNode,
5 | SetStateAction,
6 | useEffect,
7 | useState,
8 | } from 'react';
9 |
10 | interface Props {
11 | children: ReactNode;
12 | }
13 |
14 | interface AdminStatusContextType {
15 | status: string | null;
16 | setStatus: Dispatch>;
17 | }
18 |
19 | const defaultContextValue: AdminStatusContextType = {
20 | status: 'DEVELOPER',
21 | setStatus: () => {},
22 | };
23 |
24 | export const adminStatusContext =
25 | createContext(defaultContextValue);
26 |
27 | export const AdminStatusProvider = (props: Props) => {
28 | const { children } = props;
29 | const [status, setStatus] = useState('NOT_CERTIFIED');
30 |
31 | useEffect(() => {
32 | if (typeof window !== 'undefined') {
33 | const savedStatus =
34 | sessionStorage.getItem('adminStatus') ?? 'NOT_CERTIFIED';
35 | setStatus(savedStatus);
36 | }
37 | }, []);
38 |
39 | return (
40 |
41 | {children}
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/devTools/AdminStatus/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useContext, useEffect, useState } from 'react';
3 |
4 | import DropDown from '@/components/common/DropDown';
5 | import OptionTemplate from '@/components/common/OptionTemplate';
6 | import Selector from '@/components/common/Selector';
7 |
8 | import { adminStatusContext } from '../AdminContextProvider';
9 | import { StAdminStatusContainer } from './style';
10 |
11 | function AdminStatusDevtools() {
12 | const router = useRouter();
13 | const [isDropdownOpen, setIsDropdownOpen] = useState(false);
14 | const { status, setStatus } = useContext(adminStatusContext);
15 |
16 | const STATUS_SELECTION: ADMIN_STATUS[] = ['SOPT', 'MAKERS'];
17 |
18 | const isAdminStatus = (value: string): value is ADMIN_STATUS => {
19 | return [
20 | 'SUPER_USER',
21 | 'SOPT',
22 | 'MAKERS',
23 | 'NOT_CERTIFIED',
24 | 'DEVELOPER',
25 | ].includes(value);
26 | };
27 |
28 | const handleStatusChange = (newStatus: string) => {
29 | if (isAdminStatus(newStatus)) {
30 | setStatus(newStatus);
31 | sessionStorage.setItem('adminStatus', newStatus);
32 | if (newStatus === 'MAKERS') {
33 | router.push('/alarmAdmin');
34 | }
35 | } else {
36 | console.error('유효하지 않은 권한입니다.');
37 | }
38 | };
39 |
40 | return (
41 |
42 |
43 | setIsDropdownOpen(!isDropdownOpen)}
46 | />
47 | {isDropdownOpen && (
48 |
53 | )}
54 |
55 |
56 | );
57 | }
58 |
59 | export default AdminStatusDevtools;
60 |
--------------------------------------------------------------------------------
/src/components/devTools/AdminStatus/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const StAdminStatusContainer = styled.div`
4 | width: 18.5rem;
5 | `;
6 |
--------------------------------------------------------------------------------
/src/components/icons/IcDropdown.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | width?: number;
3 | height?: number;
4 | color?: string;
5 | direction?: 'LEFT' | 'RIGHT' | 'UP' | 'DOWN';
6 | }
7 |
8 | function IcDropdown(props: Props) {
9 | const {
10 | width = 14,
11 | height = 14,
12 | color = '#3C3D40',
13 | direction = 'DOWN',
14 | } = props;
15 |
16 | const rotate = () => {
17 | switch (direction) {
18 | case 'LEFT':
19 | return 'rotate(90deg)';
20 | case 'RIGHT':
21 | return 'rotate(180deg)';
22 | case 'UP':
23 | return 'rotate(-90deg)';
24 | case 'DOWN':
25 | default:
26 | return 'rotate(0deg)';
27 | }
28 | };
29 |
30 | return (
31 |
43 | );
44 | }
45 |
46 | export default IcDropdown;
47 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/CoreValue/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 |
4 | export const StValueWrapper = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | `;
8 |
9 | export const StInputWrapper = styled.div`
10 | display: flex;
11 | gap: 20px;
12 | `;
13 |
14 | // TODO : 나중에 지워라
15 | export const StDummyImageInput = styled.div`
16 | width: 224px;
17 | height: 190px;
18 | background-color: ${colors.gray800};
19 | border-radius: 10px;
20 | `;
21 |
22 | export const StInputBox = styled.div`
23 | display: flex;
24 | flex-direction: column;
25 | gap: 20px;
26 | `;
27 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/Curriculum/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 StContentWrapper = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | gap: 18px;
9 | `;
10 | export const StChipWrapper = styled.div`
11 | display: flex;
12 | gap: 6px;
13 | `;
14 | export const StList = styled.ol`
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center;
18 | gap: 8px;
19 | `;
20 | export const StItem = styled.li`
21 | display: flex;
22 | gap: 10px;
23 | `;
24 | export const StWeek = styled.label`
25 | color: ${colors.gray300};
26 | ${fontsObject.BODY_1_18_M}
27 | line-height: 48px;
28 | width: 24px;
29 | `;
30 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/Executives/SNSInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useFormContext } from 'react-hook-form';
3 |
4 | import { StInput } from '../style';
5 | import { StSNSBox } from './style';
6 |
7 | interface SNSInputProps {
8 | label: string;
9 | icon: React.FC;
10 | placeholder: string;
11 | }
12 | const SNSInput = ({ label, icon: Icon, placeholder }: SNSInputProps) => {
13 | const { register } = useFormContext();
14 | return (
15 |
16 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default SNSInput;
25 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/Executives/index.tsx:
--------------------------------------------------------------------------------
1 | import { Chip } from '@sopt-makers/ui';
2 |
3 | import { EXEC_TYPE, PART_LIST, 임원진_LIST } from '@/utils/org';
4 |
5 | import { StTitle, StWrapper } from '../style';
6 | import ExecInfo from './ExecInfo';
7 | import { StChipLabel, StChipLine, StChipWrapper } from './style';
8 |
9 | interface ExecutivesProps {
10 | selectedExec: EXEC_TYPE;
11 | onChangeSelectedExec: (member: EXEC_TYPE) => void;
12 | }
13 |
14 | const Executives = ({
15 | selectedExec,
16 | onChangeSelectedExec,
17 | }: ExecutivesProps) => {
18 | const handleSetSelectedExec = (value: EXEC_TYPE) => {
19 | onChangeSelectedExec(value);
20 | };
21 |
22 | return (
23 |
24 | 임원진
25 |
26 |
27 | 임원진
28 | {임원진_LIST.map((role) => (
29 | handleSetSelectedExec(role)}
32 | active={selectedExec === role}>
33 | {role}
34 |
35 | ))}
36 |
37 |
38 | 파트장
39 | {PART_LIST.map((part) => (
40 | handleSetSelectedExec(part)}
43 | active={selectedExec === part}>{`${part} 파트장`}
44 | ))}
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default Executives;
53 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/Executives/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 StChipWrapper = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | gap: 14px;
9 | margin-bottom: 10px;
10 | `;
11 | export const StChipLine = styled.div`
12 | display: flex;
13 | gap: 6px;
14 | `;
15 | export const StChipLabel = styled.span`
16 | margin: 9px 14px 9px 0;
17 | color: ${colors.gray300};
18 | ${fontsObject.LABEL_3_14_SB}
19 | `;
20 |
21 | export const StPhotoWrapper = styled.div`
22 | display: flex;
23 | flex-direction: column;
24 | `;
25 | // TODO : 나중에 지워라
26 | export const StDummyImageInput = styled.div`
27 | width: 168px;
28 | height: 168px;
29 | background-color: ${colors.gray800};
30 | border-radius: 100px;
31 | `;
32 |
33 | export const StSNSWrapper = styled.ul`
34 | display: flex;
35 | flex-direction: column;
36 | gap: 12px;
37 |
38 | & > span {
39 | color: ${colors.white};
40 | ${fontsObject.LABEL_3_14_SB};
41 | }
42 | `;
43 |
44 | export const StSNSBox = styled.li`
45 | display: flex;
46 | gap: 12px;
47 | align-items: center;
48 |
49 | & svg {
50 | cursor: pointer;
51 | }
52 | `;
53 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/HeaderBanner/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconInfoCircle } from '@sopt-makers/icons';
2 | import { useFormContext } from 'react-hook-form';
3 |
4 | import RequiredIcon from '../../assets/RequiredIcon';
5 | import Modal from '../../common/Modal';
6 | import useModal from '../../common/Modal/useModal';
7 | import MyDropzone from '../../MyDropzone';
8 | import { StDescription, StInputLabel, StTitle, StWrapper } from '../style';
9 | import { StContentWrapper, StInfoButton, StStretchContainer } from './style';
10 |
11 | const HeaderBanner = () => {
12 | const method = useFormContext();
13 | const { isInfoVisible, onInfoToggle } = useModal();
14 |
15 | return (
16 |
17 |
18 |
19 | 소개탭 헤더
20 |
21 |
22 |
23 |
24 |
25 |
26 | 이미지
27 |
28 |
29 |
30 | 이미지는 1920*630 크기로 올려주세요. ‘소개’탭 가장 상단에 보여지는
31 | 이미지예요.
32 |
33 |
40 |
41 |
42 |
50 |
51 | );
52 | };
53 |
54 | export default HeaderBanner;
55 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/HeaderBanner/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 |
4 | export const StStretchContainer = styled.section`
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: baseline;
8 | `;
9 | export const StContentWrapper = styled.div`
10 | display: flex;
11 | flex-direction: column;
12 | `;
13 |
14 | export const StInfoButton = styled.button`
15 | color: ${colors.white};
16 | width: 20px;
17 | `;
18 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/assets/IcLinkedinLogo.tsx:
--------------------------------------------------------------------------------
1 | const IcLinkedinLogo = () => {
2 | return (
3 |
17 | );
18 | };
19 |
20 | export default IcLinkedinLogo;
21 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/assets/IcMailLogo.tsx:
--------------------------------------------------------------------------------
1 | const IcMailLogo = () => {
2 | return (
3 |
19 | );
20 | };
21 |
22 | export default IcMailLogo;
23 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/index.tsx:
--------------------------------------------------------------------------------
1 | import { EXEC_TYPE, PART_KO } from '@/utils/org';
2 |
3 | import CoreValue from './CoreValue';
4 | import Curriculum from './Curriculum';
5 | import Executives from './Executives';
6 | import HeaderBanner from './HeaderBanner';
7 | import { StContainer } from './style';
8 |
9 | interface AboutSectionProps {
10 | selectedPart: PART_KO;
11 | onChangeSelectedPart: (part: PART_KO) => void;
12 | selectedExec: EXEC_TYPE;
13 | onChangeSelectedExec: (member: EXEC_TYPE) => void;
14 | }
15 |
16 | const AboutSection = ({
17 | selectedPart,
18 | onChangeSelectedPart,
19 | selectedExec,
20 | onChangeSelectedExec,
21 | }: AboutSectionProps) => {
22 | return (
23 |
24 |
25 |
26 |
30 |
34 |
35 | );
36 | };
37 |
38 | export default AboutSection;
39 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/AboutSection/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 | import { fontsObject } from '@sopt-makers/fonts';
4 | import { TextField } from '@sopt-makers/ui';
5 |
6 | export const StContainer = styled.section`
7 | display: flex;
8 | flex-direction: column;
9 | gap: 80px;
10 | padding: 50px 0;
11 | `;
12 |
13 | // TODO : StTitleWrapper, StTitle common 분리
14 |
15 | export const StWrapper = styled.div`
16 | display: flex;
17 | flex-direction: column;
18 | gap: 20px;
19 | `;
20 |
21 | export const StTitle = styled.h2`
22 | display: flex;
23 | align-items: center;
24 | gap: 8px;
25 |
26 | ${fontsObject.TITLE_3_24_SB}
27 | margin-bottom: 10px;
28 | color: ${colors.white};
29 | `;
30 |
31 | export const StInputLabel = styled.label`
32 | display: flex;
33 | align-items: center;
34 | gap: 4px;
35 | ${fontsObject.LABEL_3_14_SB};
36 | margin-bottom: 8px;
37 | color: ${colors.white};
38 |
39 | cursor: pointer;
40 | `;
41 |
42 | export const StDescription = styled.p`
43 | ${fontsObject.LABEL_3_14_SB};
44 | color: ${colors.gray300};
45 | `;
46 |
47 | interface StInputProps {
48 | hasValue?: boolean;
49 | }
50 |
51 | export const StInput = styled(TextField)`
52 | width: 338px;
53 | color: ${({ hasValue = true }) =>
54 | hasValue ? `${colors.white}` : `${colors.gray300}`};
55 |
56 | & label {
57 | gap: 8px;
58 | }
59 | `;
60 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/CommonSection/BrandingColor.tsx:
--------------------------------------------------------------------------------
1 | import Modal from '../common/Modal';
2 | import useModal from '../common/Modal/useModal';
3 | import {
4 | StDescription,
5 | StInputWrapper,
6 | StTitle,
7 | StTitleWrapper,
8 | StWrapper,
9 | } from '../style';
10 | import BrandingSubColor from './BrandingSubColor';
11 | import ColorInputField from './ColorInputField';
12 | import { StStretchContainer } from './style';
13 |
14 | const BrandingColor = () => {
15 | const { isInfoVisible, onInfoToggle } = useModal();
16 |
17 | return (
18 |
19 |
20 |
21 | 브랜딩 컬러
22 | 다크 모드를 고려하여 선정해주세요.
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {' '}
31 |
39 |
40 | );
41 | };
42 |
43 | export default BrandingColor;
44 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/CommonSection/BrandingSubColor.tsx:
--------------------------------------------------------------------------------
1 | import { IconInfoCircle } from '@sopt-makers/icons';
2 | import { type MouseEvent } from 'react';
3 | import { useFormContext } from 'react-hook-form';
4 |
5 | import { VALIDATION_CHECK } from '@/utils/org';
6 |
7 | import { StInputLabel } from '../AboutSection/style';
8 | import RequiredIcon from '../assets/RequiredIcon';
9 | import { StInput, StInputBox } from '../style';
10 | import {
11 | StColorWrapper,
12 | StInfoButton,
13 | StSubColorDescription,
14 | StSubColorPreview,
15 | } from './style';
16 | import { expandHexColor } from './utils';
17 |
18 | const BrandingSubColor = ({
19 | onInfoToggle,
20 | }: {
21 | onInfoToggle: (e: MouseEvent) => void;
22 | }) => {
23 | const {
24 | register,
25 | formState: { errors },
26 | watch,
27 | setValue,
28 | } = useFormContext();
29 |
30 | return (
31 |
32 |
33 |
34 | 서브 컬러 (강조 그레이 컬러)
35 |
36 |
37 |
38 | 강조하고 싶은 박스의 그레이 컬러를 지정해주세요.
39 |
40 |
41 |
42 |
43 |
55 |
59 | setValue('brandingColor.point', e.target.value.replace('#', ''))
60 | }
61 | />
62 |
63 |
64 | );
65 | };
66 |
67 | export default BrandingSubColor;
68 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/CommonSection/ColorInputField.tsx:
--------------------------------------------------------------------------------
1 | import { useFormContext } from 'react-hook-form';
2 |
3 | import { VALIDATION_CHECK } from '@/utils/org';
4 |
5 | import { StInput } from '../style';
6 | import { StColorPreview, StColorWrapper } from './style';
7 | import { expandHexColor } from './utils';
8 |
9 | interface ColorInputFieldProps {
10 | label: string;
11 | id: string;
12 | }
13 |
14 | const ColorInputField = ({ label, id }: ColorInputFieldProps) => {
15 | const {
16 | register,
17 | formState: { errors },
18 | watch,
19 | setValue,
20 | } = useFormContext();
21 | const [brandingColor, color] = id.split('.');
22 |
23 | return (
24 |
25 |
40 | setValue(id, e.target.value.replace('#', ''))}
44 | />
45 |
46 | );
47 | };
48 |
49 | export default ColorInputField;
50 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/CommonSection/index.tsx:
--------------------------------------------------------------------------------
1 | import { StContainer } from '../style';
2 | import type { Group } from '../types';
3 | import BrandingColor from './BrandingColor';
4 | import GenerationInformation from './GenerationInformation';
5 | import RecruitSchedule from './RecruitSchedule';
6 |
7 | interface CommonSectionProps {
8 | group: Group;
9 | onChangeGroup: (group: Group) => void;
10 | }
11 |
12 | const CommonSection = ({ group, onChangeGroup }: CommonSectionProps) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default CommonSection;
23 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/CommonSection/utils.ts:
--------------------------------------------------------------------------------
1 | const isValidHexColor = (hex: string) => {
2 | const hexRegex = /^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
3 | return hexRegex.test(hex);
4 | };
5 |
6 | export const expandHexColor = (hex: string) => {
7 | if (!isValidHexColor(hex)) return '#ffffff';
8 |
9 | if (hex.length === 3) {
10 | return `#${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
11 | }
12 |
13 | return `#${hex}`;
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/HomeSection.tsx:
--------------------------------------------------------------------------------
1 | import { ToastProvider } from '@sopt-makers/ui';
2 |
3 | import NewsSection from '@/components/org/OrgAdmin/HomeSection/NewsSection';
4 | import PartIntroSection from '@/components/org/OrgAdmin/HomeSection/PartIntroSection';
5 | import { useAdminInfoQuery } from '@/components/org/OrgAdmin/HomeSection/queries';
6 | import {
7 | StContainer,
8 | StWrapper,
9 | } from '@/components/org/OrgAdmin/HomeSection/style';
10 | import { PART_KO } from '@/utils/org';
11 |
12 | type HomeSectionProps = {
13 | selectedIntroPart: PART_KO;
14 | onChangeIntroPart: (part: PART_KO) => void;
15 | };
16 |
17 | const HomeSection = ({
18 | selectedIntroPart,
19 | onChangeIntroPart,
20 | }: HomeSectionProps) => {
21 | const { data } = useAdminInfoQuery();
22 |
23 | return (
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default HomeSection;
39 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/ImageInput.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 | import { fontsObject } from '@sopt-makers/fonts';
4 | import { HTMLAttributes } from 'react';
5 | import { useFormContext } from 'react-hook-form';
6 |
7 | import MyDropZone from '../MyDropzone';
8 |
9 | interface ImageInputProps extends HTMLAttributes {
10 | label: string;
11 | description?: string;
12 | }
13 |
14 | const ImageInput = ({ label, description }: ImageInputProps) => {
15 | const method = useFormContext();
16 |
17 | return (
18 |
19 |
20 | 이미지
21 | *
22 |
23 | {description}
24 |
25 |
32 |
33 | );
34 | };
35 |
36 | export default ImageInput;
37 |
38 | const StInputContainer = styled.div`
39 | display: flex;
40 | flex-direction: column;
41 | gap: 8px;
42 | `;
43 |
44 | const StLabel = styled.p`
45 | ${fontsObject.LABEL_3_14_SB};
46 | color: ${colors.white};
47 | `;
48 |
49 | const StRequiredIcon = styled.span`
50 | color: ${colors.secondary};
51 | margin-left: 4px;
52 | `;
53 |
54 | const StDescription = styled.p`
55 | ${fontsObject.LABEL_4_12_SB};
56 | color: ${colors.gray300};
57 |
58 | padding-bottom: 5px;
59 | `;
60 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/Modal.style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 | import { fontsObject } from '@sopt-makers/fonts';
4 | import { Button } from '@sopt-makers/ui';
5 |
6 | export const StCancelButton = styled(Button)`
7 | background-color: ${colors.gray600};
8 | color: ${colors.white};
9 |
10 | &:hover {
11 | color: ${colors.black};
12 | }
13 | `;
14 |
15 | export const StAddButton = styled(Button)`
16 | background-color: ${colors.white};
17 | color: ${colors.black};
18 | `;
19 |
20 | export const StAddModalContainer = styled.div`
21 | display: flex;
22 | flex-direction: column;
23 | gap: 36px;
24 |
25 | width: 500px;
26 |
27 | padding: 22px 32px 43px 32px;
28 |
29 | background-color: ${colors.gray900};
30 | border-radius: 12px;
31 | `;
32 |
33 | export const StAddModalBtnWrapper = styled.div`
34 | place-self: end;
35 |
36 | display: flex;
37 | gap: 12px;
38 | `;
39 |
40 | export const StInputContainer = styled.div`
41 | display: flex;
42 | flex-direction: column;
43 | gap: 8px;
44 | `;
45 |
46 | export const StLabel = styled.p`
47 | ${fontsObject.LABEL_3_14_SB};
48 | color: ${colors.white};
49 | `;
50 |
51 | export const StRequiredIcon = styled.span`
52 | color: ${colors.secondary};
53 | margin-left: 4px;
54 | `;
55 |
56 | export const StDescription = styled.p`
57 | ${fontsObject.LABEL_4_12_SB};
58 | color: ${colors.gray300};
59 |
60 | padding-bottom: 5px;
61 | `;
62 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/NewsItem.tsx:
--------------------------------------------------------------------------------
1 | import { IconTrash } from '@sopt-makers/icons';
2 |
3 | import { StNewsItem } from '@/components/org/OrgAdmin/HomeSection/style';
4 |
5 | type NewsItemProps = {
6 | title: string;
7 | onDelete?: () => void;
8 | };
9 |
10 | const NewsItem = ({ title, onDelete }: NewsItemProps) => {
11 | return (
12 |
13 | {title}
14 | e.key === 'Enter' && onDelete?.()}
19 | onClick={onDelete}
20 | color="white"
21 | />
22 |
23 | );
24 | };
25 |
26 | export default NewsItem;
27 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/SampleView.tsx:
--------------------------------------------------------------------------------
1 | import { colors } from '@sopt-makers/colors';
2 | import { IconInfoCircle } from '@sopt-makers/icons';
3 | import Image, { StaticImageData } from 'next/image';
4 |
5 | import { IcModalClose } from '@/assets/icons';
6 | import {
7 | StDescription,
8 | StDescription2,
9 | StImgTitle,
10 | StImgWrapper,
11 | StImgWrapperTitle,
12 | } from '@/components/org/OrgAdmin/HomeSection/style';
13 |
14 | interface SampleViewProps {
15 | category: string;
16 | title: string;
17 | description: string;
18 | src: StaticImageData;
19 | }
20 |
21 | const SampleView = ({ category, title, description, src }: SampleViewProps) => {
22 | return (
23 |
24 |
25 |
26 |
27 | {category}
28 |
29 |
30 |
31 | {title}
32 | {description}
33 |
34 |
35 | );
36 | };
37 |
38 | export default SampleView;
39 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import config from '@/configs/config';
4 | import { getToken } from '@/utils/auth';
5 | import { ACTIVITY_GENERATION } from '@/utils/generation';
6 |
7 | import { fetcher } from '../api';
8 |
9 | export const getAdminInfo = async () => {
10 | const { data } = await fetcher.GET('/admin', {
11 | params: {
12 | query: {
13 | generation: ACTIVITY_GENERATION,
14 | },
15 | },
16 | });
17 |
18 | return data;
19 | };
20 |
21 | export const postNews = async (formData: FormData) => {
22 | const res = await axios.post(
23 | `${config.ORG_API_URL}/v2/admin/news`,
24 | formData,
25 | {
26 | headers: {
27 | Authorization: getToken('ACCESS'),
28 | 'Content-Type': 'multipart/form-data',
29 | },
30 | },
31 | );
32 |
33 | return res;
34 | };
35 |
36 | export const deleteNews = async (id: number) => {
37 | const res = await axios.post(
38 | `${config.ORG_API_URL}/v2/admin/news/delete`,
39 | {
40 | id,
41 | },
42 | {
43 | headers: {
44 | Authorization: getToken('ACCESS'),
45 | },
46 | },
47 | );
48 |
49 | return res;
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/constant.ts:
--------------------------------------------------------------------------------
1 | export const NEWS = [
2 | { id: 1, title: 'Do SOPT OT' },
3 | { id: 2, title: 'SOPT effect : 창업가 초청 토크 세션' },
4 | { id: 3, title: '[매쉬업엔젤스 X SOPT] Open Office Hours' },
5 | { id: 4, title: 'MIND 23 : IT PEOPLE CONFERENCE' },
6 | { id: 5, title: 'DO SOPT 1차 행사' },
7 | ];
8 |
9 | export const PARTS = [
10 | '기획',
11 | '디자인',
12 | '안드로이드',
13 | 'iOS',
14 | '웹',
15 | '서버',
16 | ] as const;
17 |
18 | export const PARTS_FILTER = {
19 | 기획: 'pm',
20 | 디자인: 'de',
21 | 안드로이드: 'an',
22 | IOS: 'io',
23 | 웹: 'we',
24 | 서버: 'sv',
25 | } as const;
26 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/HomeSection/queries.ts:
--------------------------------------------------------------------------------
1 | import { type ToastOptionType, useToast } from '@sopt-makers/ui';
2 | import { useMutation, useQuery, useQueryClient } from 'react-query';
3 |
4 | import {
5 | deleteNews,
6 | getAdminInfo,
7 | postNews,
8 | } from '@/components/org/OrgAdmin/HomeSection/api';
9 |
10 | const TOAST_OPTION: Record<'success' | 'error', ToastOptionType> = {
11 | success: { icon: 'success', content: '성공적으로 추가되었어요' },
12 | error: { icon: 'error', content: '추가에 실패했어요' },
13 | };
14 |
15 | export const useAdminInfoQuery = () => {
16 | return useQuery({
17 | queryKey: ['admin'],
18 | queryFn: getAdminInfo,
19 | });
20 | };
21 |
22 | export const useAddNewsMutation = () => {
23 | const queryClient = useQueryClient();
24 | const { open } = useToast();
25 |
26 | return useMutation({
27 | mutationFn: (formData: FormData) => postNews(formData),
28 | onSuccess: () => {
29 | queryClient.invalidateQueries({
30 | queryKey: ['admin'],
31 | });
32 |
33 | open(TOAST_OPTION.success);
34 | },
35 | });
36 | };
37 |
38 | export const useDeleteNewsMutation = () => {
39 | const queryClient = useQueryClient();
40 | const { open } = useToast();
41 | const option: ToastOptionType = {
42 | icon: 'success',
43 | content: '성공적으로 삭제되었어요.',
44 | };
45 |
46 | return useMutation({
47 | mutationFn: (id: number) => deleteNews(id),
48 | onSuccess: () => {
49 | queryClient.invalidateQueries({
50 | queryKey: ['admin'],
51 | });
52 |
53 | open(TOAST_OPTION.success);
54 | },
55 | onError: () => {
56 | open(TOAST_OPTION.error);
57 | },
58 | });
59 | };
60 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/MyDropzone/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 | import { fontsObject } from '@sopt-makers/fonts';
4 | import { IconImagePlus } from '@sopt-makers/icons';
5 |
6 | export const StImgButtonWrapper = styled.div`
7 | display: flex;
8 | flex-direction: column;
9 | gap: 10px;
10 | `;
11 |
12 | interface StImgButtonProps {
13 | isError: boolean;
14 | width: string;
15 | height: string;
16 | shape: 'square' | 'circle';
17 | }
18 |
19 | export const StImgButton = styled.div`
20 | ${fontsObject.BODY_4_13_M}
21 |
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | width: ${({ width }) => width};
26 | height: ${({ height }) => height};
27 | margin-top: 13px;
28 | color: ${colors.white};
29 | background-color: ${colors.gray800};
30 | border: ${({ isError }) => (isError ? `1px solid ${colors.error}` : 'none')};
31 | border-radius: ${({ shape }) => (shape === 'square' ? '10px' : '50%')};
32 | cursor: pointer;
33 | overflow: hidden;
34 | `;
35 |
36 | export const StImgIcon = styled(IconImagePlus)`
37 | width: 24px;
38 | height: 24px;
39 | color: ${colors.white};
40 | `;
41 |
42 | export const StImgPreview = styled.img`
43 | max-width: 100%;
44 | height: 100%;
45 | object-fit: contain;
46 | color: ${colors.white};
47 | border-radius: 10px;
48 | `;
49 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/PartCategory/index.tsx:
--------------------------------------------------------------------------------
1 | import { Chip } from '@sopt-makers/ui';
2 |
3 | import { PART_KO, PART_LIST } from '@/utils/org';
4 |
5 | import { StPartCategoryWrapper } from './style';
6 |
7 | interface PartCategoryProps {
8 | selectedPart: PART_KO;
9 | onSetSelectedPart: (part: PART_KO) => void;
10 | }
11 | const PartCategory = ({
12 | selectedPart,
13 | onSetSelectedPart,
14 | }: PartCategoryProps) => {
15 | return (
16 |
17 | {PART_LIST.map((part) => (
18 | onSetSelectedPart(part)}
21 | active={selectedPart === part}>
22 | {part}
23 |
24 | ))}
25 |
26 | );
27 | };
28 |
29 | export default PartCategory;
30 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/PartCategory/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const StPartCategoryWrapper = styled.div`
4 | display: flex;
5 | gap: 6px;
6 | align-items: center;
7 | `;
8 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/RecruitSection/Header.tsx:
--------------------------------------------------------------------------------
1 | import { IconInfoCircle } from '@sopt-makers/icons';
2 | import { useFormContext } from 'react-hook-form';
3 |
4 | import RequiredIcon from '../assets/RequiredIcon';
5 | import Modal from '../common/Modal';
6 | import useModal from '../common/Modal/useModal';
7 | import MyDropzone from '../MyDropzone';
8 | import {
9 | StDescription,
10 | StLabel,
11 | StTitle,
12 | StTitleWrapper,
13 | StWrapper,
14 | } from '../style';
15 | import { StInfoButton, StLabelWrapper, StStretchContainer } from './style';
16 |
17 | const Header = () => {
18 | const method = useFormContext();
19 | const { isInfoVisible, onInfoToggle } = useModal();
20 |
21 | return (
22 |
23 |
24 |
25 | 지원하기탭 헤더
26 |
27 |
28 |
29 |
30 |
31 | 이미지
32 |
33 |
34 |
35 | 이미지는 1920*580 크기로 올려주세요. ‘지원하기’탭 가장 상단에 보여지는
36 | 이미지예요.
37 |
38 |
39 |
40 |
48 |
49 | );
50 | };
51 |
52 | export default Header;
53 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/RecruitSection/index.tsx:
--------------------------------------------------------------------------------
1 | import { PART_KO } from '@/utils/org';
2 |
3 | import { StContainer } from '../style';
4 | import Fna from './Fna';
5 | import Header from './Header';
6 | import PartCurriculum from './PartCurriculum';
7 |
8 | interface RecruitSectionProps {
9 | curriculumPart: PART_KO;
10 | onChangeCurriculumPart: (part: PART_KO) => void;
11 | fnaPart: PART_KO;
12 | onChangeFnaPart: (part: PART_KO) => void;
13 | }
14 |
15 | const RecruitSection = ({
16 | curriculumPart,
17 | onChangeCurriculumPart,
18 | fnaPart,
19 | onChangeFnaPart,
20 | }: RecruitSectionProps) => {
21 | return (
22 |
23 |
24 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default RecruitSection;
34 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/RecruitSection/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 |
4 | export const StStretchContainer = styled.section`
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: center;
8 | `;
9 | export const StLabelWrapper = styled.div`
10 | display: flex;
11 | gap: 4px;
12 | align-items: center;
13 | margin: 30px 0px 8px;
14 | `;
15 |
16 | export const StTextAreaWrapper = styled.div`
17 | display: flex;
18 | flex-direction: column;
19 | gap: 20px;
20 | margin-top: 18px;
21 | `;
22 |
23 | export const StFnaWrapper = styled.div`
24 | display: flex;
25 | flex-direction: column;
26 | gap: 8px;
27 | `;
28 |
29 | export const StInfoButton = styled.button`
30 | color: ${colors.white};
31 | width: 20px;
32 | `;
33 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/api.ts:
--------------------------------------------------------------------------------
1 | import createFetch from 'openapi-fetch';
2 |
3 | import { paths } from '@/__generated__/api';
4 | import { AddAdminRequestDto } from '@/__generated__/org-types/data-contracts';
5 | import config from '@/configs/config';
6 | import { getToken } from '@/utils/auth';
7 |
8 | export const fetcher = createFetch({
9 | baseUrl: `${config.ORG_API_URL}/v2`,
10 | headers: {
11 | Authorization: getToken('ACCESS'),
12 | },
13 | });
14 |
15 | export const sendData = async (data: AddAdminRequestDto) => {
16 | const res = await fetcher.POST('/admin', {
17 | body: data,
18 | });
19 |
20 | return res.data;
21 | };
22 |
23 | export const sendPresignedURL = async (url: string, data: BodyInit) => {
24 | const res = await fetch(url, {
25 | method: 'PUT',
26 | body: data,
27 | });
28 |
29 | return res;
30 | };
31 |
32 | export const sendDataConfirm = async (data: { generation: number }) => {
33 | const res = await fetcher.POST('/admin/confirm', {
34 | body: data,
35 | });
36 |
37 | return res;
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/assets/RequiredIcon.tsx:
--------------------------------------------------------------------------------
1 | const RequiredIcon = () => {
2 | return (
3 |
14 | );
15 | };
16 |
17 | export default RequiredIcon;
18 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/assets/SubmitIcon.tsx:
--------------------------------------------------------------------------------
1 | const SubmitIcon = () => {
2 | return (
3 |
17 | );
18 | };
19 |
20 | export default SubmitIcon;
21 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/assets/imgSubColorInfo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/src/components/org/OrgAdmin/assets/imgSubColorInfo.png
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/common/ActionModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { CheckBox } from '@sopt-makers/ui';
2 | import { type HTMLAttributes, useState } from 'react';
3 | import type { FieldValues, SubmitHandler } from 'react-hook-form';
4 |
5 | import Modal from '@/components/common/modal';
6 |
7 | import {
8 | StActionButton,
9 | StCancelButton,
10 | StModalBtnWrapper,
11 | StModalContainer,
12 | } from './style';
13 |
14 | interface ActionModalProps extends Omit, 'id'> {
15 | isOpen: boolean;
16 | onCancel?: () => void;
17 | onAction?: () => void | SubmitHandler;
18 | variant: 'add' | 'delete' | 'deploy';
19 | alertText: string;
20 | description?: string;
21 | }
22 |
23 | export const ActionModal = ({
24 | isOpen,
25 | onCancel,
26 | onAction,
27 | variant,
28 | alertText,
29 | description,
30 | }: ActionModalProps) => {
31 | const [checked, setChecked] = useState(false);
32 |
33 | return (
34 | isOpen && (
35 |
36 |
37 | {alertText}
38 | {description}
39 | setChecked((prev) => !prev)}
43 | />
44 |
45 | {
47 | onCancel && onCancel();
48 | setChecked(false);
49 | }}>
50 | 취소
51 |
52 | {
56 | setChecked(false);
57 | onAction && onAction();
58 | }}>
59 | {variant === 'add'
60 | ? '추가'
61 | : variant === 'delete'
62 | ? '삭제'
63 | : '배포'}
64 |
65 |
66 |
67 |
68 | )
69 | );
70 | };
71 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/common/ActionModal/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '@sopt-makers/colors';
3 | import { fontsObject } from '@sopt-makers/fonts';
4 | import { Button } from '@sopt-makers/ui';
5 |
6 | export const StModalContainer = styled.div`
7 | display: flex;
8 | flex-direction: column;
9 | gap: 12px;
10 |
11 | padding: 24px;
12 |
13 | & > h2 {
14 | ${fontsObject.TITLE_4_20_SB};
15 | color: ${colors.gray10};
16 | }
17 |
18 | & > p {
19 | ${fontsObject.BODY_2_16_R};
20 | color: ${colors.gray100};
21 | white-space: pre-line;
22 | }
23 | `;
24 |
25 | export const StModalBtnWrapper = styled.div`
26 | place-self: end;
27 |
28 | display: flex;
29 | gap: 12px;
30 |
31 | padding-top: 24px;
32 | `;
33 |
34 | export const StCancelButton = styled(Button)`
35 | background-color: ${colors.gray600};
36 | color: ${colors.white};
37 |
38 | &:hover {
39 | color: ${colors.black};
40 | }
41 | `;
42 |
43 | export const StActionButton = styled(Button)<{
44 | btntype: 'add' | 'delete' | 'deploy';
45 | }>`
46 | color: ${(props) => (props.btntype === 'add' ? colors.black : colors.white)};
47 |
48 | background-color: ${(props) =>
49 | props.btntype === 'add' ? colors.white : colors.error};
50 |
51 | &:disabled {
52 | cursor: default;
53 | }
54 | `;
55 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/common/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconInfoCircle } from '@sopt-makers/icons';
2 | import { MouseEvent } from 'react';
3 |
4 | import {
5 | StInfoCloseButton,
6 | StInfoDescription,
7 | StInfoImg,
8 | StInfoSubDescription,
9 | StInfoTitle,
10 | StInfoWrapper,
11 | } from './style';
12 |
13 | interface ModalProps {
14 | title: string;
15 | description: string;
16 | subDescription: string;
17 | imgSrc: string;
18 | isInfoVisible: boolean;
19 | onInfoToggle: (e: MouseEvent) => void;
20 | }
21 |
22 | const Modal = ({
23 | title,
24 | description,
25 | subDescription,
26 | imgSrc,
27 | isInfoVisible,
28 | onInfoToggle,
29 | }: ModalProps) => {
30 | return (
31 |
32 |
33 |
34 | {title}
35 |
36 | ✕
37 |
38 |
39 | {description}
40 | {subDescription}
41 |
42 |
43 | );
44 | };
45 |
46 | export default Modal;
47 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/common/Modal/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 | interface StInfoWrapperProps {
8 | isVisible: boolean;
9 | }
10 |
11 | export const StInfoWrapper = styled.article`
12 | width: fit-content;
13 | position: relative;
14 | border-radius: 12px;
15 | padding: 22px 32px 38px;
16 | background-color: ${colors.gray900};
17 | transform: ${({ isVisible }) =>
18 | isVisible ? 'translateX(0)' : 'translateX(100%)'};
19 | opacity: ${({ isVisible }) => (isVisible ? 1 : 0)};
20 | transition: all 0.5s ease-in-out;
21 | z-index: ${zIndex.modal};
22 | `;
23 |
24 | export const StInfoTitle = styled.h2`
25 | ${fontsObject.HEADING_4_24_B};
26 |
27 | display: flex;
28 | align-items: center;
29 | gap: 8px;
30 | margin-bottom: 22px;
31 | color: ${colors.gray10};
32 |
33 | & svg {
34 | width: 28px;
35 | }
36 | `;
37 |
38 | export const StInfoCloseButton = styled.button`
39 | position: absolute;
40 | right: 32px;
41 | top: 22px;
42 | ${fontsObject.HEADING_4_24_B};
43 |
44 | color: ${colors.gray10};
45 | `;
46 |
47 | export const StInfoDescription = styled.p`
48 | ${fontsObject.LABEL_3_14_SB};
49 |
50 | margin-bottom: 8px;
51 | color: ${colors.white};
52 | `;
53 |
54 | export const StInfoSubDescription = styled.p`
55 | ${fontsObject.LABEL_4_12_SB};
56 |
57 | margin-bottom: 14px;
58 | color: ${colors.gray300};
59 | `;
60 |
61 | export const StInfoImg = styled.img`
62 | color: ${colors.white};
63 | `;
64 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/common/Modal/useModal.ts:
--------------------------------------------------------------------------------
1 | import { MouseEvent, useState } from 'react';
2 |
3 | const useModal = () => {
4 | const [isInfoVisible, setIsInfoVisible] = useState(false);
5 |
6 | const onInfoToggle = (e: MouseEvent) => {
7 | e.preventDefault();
8 | setIsInfoVisible((prev) => !prev);
9 | };
10 |
11 | return { isInfoVisible, onInfoToggle };
12 | };
13 |
14 | export default useModal;
15 |
--------------------------------------------------------------------------------
/src/components/org/OrgAdmin/types.ts:
--------------------------------------------------------------------------------
1 | export type Group = 'OB' | 'YB';
2 |
--------------------------------------------------------------------------------
/src/components/org/RecruitAdmin/index.tsx:
--------------------------------------------------------------------------------
1 | function RecruitAdmin() {
2 | return <>recruit admin>;
3 | }
4 |
5 | export default RecruitAdmin;
6 |
--------------------------------------------------------------------------------
/src/components/org/RecruitAdmin/style.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sopt-makers/sopt-operation-frontend/a8ef7c511ff6a1a503159b0ec8568601f172ee73/src/components/org/RecruitAdmin/style.ts
--------------------------------------------------------------------------------
/src/configs/config.ts:
--------------------------------------------------------------------------------
1 | const API_URL =
2 | process.env.NEXT_PUBLIC_API_URL === 'PRODUCTION'
3 | ? 'https://operation.api.sopt.org/api/v1'
4 | : 'https://operation.api.dev.sopt.org/api/v1';
5 | const CLIENT_URL = 'https://operation.sopt.org';
6 |
7 | const ORG_API_URL =
8 | process.env.NEXT_PUBLIC_API_URL === 'PRODUCTION'
9 | ? 'https://api.sopt.org'
10 | : 'https://api-dev.sopt.org';
11 |
12 | const config = {
13 | ENV_STATUS: process.env.NODE_ENV,
14 | API_URL,
15 | CLIENT_URL,
16 | ORG_API_URL,
17 | };
18 |
19 | export default config;
20 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const HEADER_LIST = [
2 | '진행 상태',
3 | '배너 노출 위치',
4 | '콘텐츠 유형',
5 | '광고 요청자',
6 | '시작날짜',
7 | '종료날짜',
8 | '',
9 | ];
10 |
11 | export const ITEM_DUMMY_LIST = [
12 | {
13 | status: '진행 예정',
14 | bannerLocation: '플그 커뮤니티',
15 | contentType: '프로덕트 홍보',
16 | requester: '마케팅 오거나이저',
17 | startedAt: '2025.03.28',
18 | endedAt: '2025.03.28',
19 | },
20 | ];
21 |
22 | export const BANNER_TAB_FILTER_LIST: string[] = [
23 | '진행 상태 순',
24 | '시작날짜 빠른 순',
25 | '종료날짜 빠른 순',
26 | ];
27 |
28 | export const DEFAULT_BANNER_LIST_LIMIT = 20;
29 | export const DEFAULT_BANNER_LIST_MAX = 1000;
30 | export const INIT_BANNER_LIST_PAGE = 1;
31 |
--------------------------------------------------------------------------------
/src/data/queryData.ts:
--------------------------------------------------------------------------------
1 | export const PAGE_SIZE = 20;
2 |
--------------------------------------------------------------------------------
/src/data/sessionData.ts:
--------------------------------------------------------------------------------
1 | export const attendanceInit: Attendance = {
2 | subAttendanceId: 0,
3 | round: 0,
4 | status: 'ABSENT',
5 | updateAt: '-',
6 | };
7 |
8 | export const scoreDetailAttendanceInit: ScoreDetailAttendance = {
9 | round: 0,
10 | status: '결석',
11 | date: '-',
12 | };
13 |
14 | export const subLectureInit: SubLecture = {
15 | subLectureId: 0,
16 | round: 0,
17 | startAt: null,
18 | code: null,
19 | };
20 |
21 | export const attendanceOptions: Record<
22 | string,
23 | Array<{
24 | label: string;
25 | value: ATTEND_STATUS;
26 | }>
27 | > = {
28 | first: [
29 | { label: '1차 출석', value: 'ATTENDANCE' },
30 | { label: '1차 결석', value: 'ABSENT' },
31 | ],
32 | second: [
33 | { label: '2차 출석', value: 'ATTENDANCE' },
34 | { label: '2차 결석', value: 'ABSENT' },
35 | ],
36 | };
37 |
--------------------------------------------------------------------------------
/src/hooks/useBooleanState.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export const useBooleanState = () => {
4 | const [flag, setFlag] = useState(false);
5 |
6 | const setTrue = () => {
7 | setFlag(true);
8 | };
9 |
10 | const setFalse = () => {
11 | setFlag(false);
12 | };
13 |
14 | const toggle = () => {
15 | setFlag((prev) => !prev);
16 | };
17 |
18 | return { flag, setTrue, setFalse, toggle };
19 | };
20 |
--------------------------------------------------------------------------------
/src/hooks/useCreateSession.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from 'react-query';
2 | import { useRecoilValue } from 'recoil';
3 |
4 | import { currentGenerationState } from '@/recoil/atom';
5 | import { postNewSession } from '@/services/api/lecture';
6 | import { partTranslator } from '@/utils/session';
7 |
8 | interface MutationInput {
9 | newData: SessionBase;
10 | }
11 |
12 | /** 세션 생성 시 작동하는 커스텀 훅
13 | * @param part 선택한 파트
14 | * @return createSession 함수를 return 하여 외부에서 사용할 수 있도록 함.
15 | */
16 | export const useCreateSession = (part: string) => {
17 | const queryClient = useQueryClient();
18 |
19 | const currentGeneration = useRecoilValue(currentGenerationState);
20 |
21 | const mutation = useMutation(
22 | ({ newData }) => postNewSession(newData),
23 | {
24 | onSuccess: () => {
25 | queryClient.invalidateQueries([
26 | 'sessionList',
27 | parseInt(currentGeneration),
28 | partTranslator[part],
29 | ]);
30 | },
31 | },
32 | );
33 |
34 | /** useCreateSession 내의 useMutation 을 실행시키는 함수
35 | * @param submitContents 세션 생성에 필요한 정보를 담은 객체
36 | */
37 | const createSession = (submitContents: SessionBase) => {
38 | mutation.mutate({ newData: submitContents });
39 | };
40 |
41 | return { createSession };
42 | };
43 |
--------------------------------------------------------------------------------
/src/hooks/useInput.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, useReducer } from 'react';
2 |
3 | const reducer = (state: T, action: EventTarget & HTMLInputElement): T => ({
4 | ...state,
5 | [action.name]: action.value,
6 | });
7 |
8 | const useInput = (initValue: T) => {
9 | const [state, dispatch] = useReducer<
10 | React.Reducer
11 | >(reducer, initValue);
12 |
13 | const onChange = (e: ChangeEvent) => {
14 | dispatch(e.target);
15 | };
16 |
17 | return { state, onChange };
18 | };
19 |
20 | export default useInput;
21 |
--------------------------------------------------------------------------------
/src/hooks/useObserver.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react';
2 | import { FetchNextPageOptions, InfiniteQueryObserverResult } from 'react-query';
3 |
4 | interface UseObserver {
5 | target: RefObject;
6 | fetchNextPage: (
7 | options?: FetchNextPageOptions,
8 | ) => Promise>;
9 | root?: HTMLElement;
10 | rootMargin?: string;
11 | threshold?: number;
12 | }
13 |
14 | const useObserver = (props: UseObserver) => {
15 | const {
16 | target, // 감지할 대상
17 | fetchNextPage,
18 | root = null, // 교차할 부모 요소, default: documentElement
19 | rootMargin = '0px', // root와 target이 감지하는 여백의 거리
20 | threshold = 1.0, // 임계점 - 1.0이면 root내에서 target이 100% 보여질 때 callback 실행
21 | } = props;
22 |
23 | const onIntersect: IntersectionObserverCallback = ([entry]) =>
24 | entry.isIntersecting && fetchNextPage();
25 |
26 | useEffect(() => {
27 | let observer: IntersectionObserver;
28 |
29 | if (target && target.current) {
30 | observer = new IntersectionObserver(onIntersect, {
31 | root,
32 | rootMargin,
33 | threshold,
34 | });
35 | observer.observe(target.current);
36 | }
37 |
38 | return () => observer && observer.disconnect();
39 | }, [onIntersect, root, rootMargin, target, threshold]);
40 | };
41 |
42 | export default useObserver;
43 |
--------------------------------------------------------------------------------
/src/hooks/useRecoilGenerationSSR.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useRecoilState } from 'recoil';
3 |
4 | import { currentGenerationState } from '@/recoil/atom';
5 | import { ACTIVITY_GENERATION } from '@/utils/generation';
6 |
7 | export const useRecoilGenerationSSR = () => {
8 | const [isInitial, setIsInitial] = useState(true);
9 | const [value, setValue] = useRecoilState(currentGenerationState);
10 |
11 | useEffect(() => {
12 | setIsInitial(false);
13 | }, []);
14 |
15 | return [isInitial ? ACTIVITY_GENERATION : value, setValue] as const;
16 | };
17 |
--------------------------------------------------------------------------------
/src/hooks/useUnauthorizedStatus.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useEffect } from 'react';
3 |
4 | export const useUnauthorizedStatus = (status: ADMIN_STATUS) => {
5 | const router = useRouter();
6 |
7 | useEffect(() => {
8 | if (
9 | typeof window !== 'undefined' &&
10 | sessionStorage.getItem('adminStatus') === status
11 | ) {
12 | alert('접근 권한이 없는 계정입니다.');
13 | router.back();
14 | }
15 | }, [router, status]);
16 | };
17 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@sopt-makers/ui/dist/index.css';
2 |
3 | import { Global, ThemeProvider } from '@emotion/react';
4 | import { DialogProvider, ToastProvider } from '@sopt-makers/ui';
5 | import type { AppProps } from 'next/app';
6 | import Head from 'next/head';
7 | import { useRouter } from 'next/router';
8 | import { useEffect } from 'react';
9 | import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
10 | import { ReactQueryDevtools } from 'react-query/devtools';
11 | import { RecoilRoot } from 'recoil';
12 |
13 | import Layout from '@/components/common/Layout';
14 | import { AdminStatusProvider } from '@/components/devTools/AdminContextProvider';
15 | import global from '@/styles/global';
16 | import theme from '@/styles/theme';
17 | import { getToken } from '@/utils/auth';
18 |
19 | const client = new QueryClient({
20 | defaultOptions: {
21 | queries: {
22 | refetchOnWindowFocus: false,
23 | },
24 | },
25 | });
26 |
27 | export default function App({ Component, pageProps }: AppProps) {
28 | const router = useRouter();
29 |
30 | useEffect(() => {
31 | if (!getToken('ACCESS')) {
32 | router.replace('/');
33 | }
34 | }, [router]);
35 |
36 | return (
37 | <>
38 |
39 |
40 | {process.env.NEXT_PUBLIC_API_URL !== 'PRODUCTION'
41 | ? '[DEV] SOPT Admin'
42 | : 'SOPT Admin'}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | >
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from 'next/document';
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 |
4 | type Data = {
5 | name: string;
6 | };
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse,
11 | ) {
12 | res.status(200).json({ name: 'John Doe' });
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/attendanceAdmin/session/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import CreateSessionModal from '@/components/attendanceAdmin/session/CreateSessionModal';
4 | import SessionList from '@/components/attendanceAdmin/session/SessionList';
5 | import FloatingButton from '@/components/common/FloatingButton';
6 | import Modal from '@/components/common/modal';
7 | import { useUnauthorizedStatus } from '@/hooks/useUnauthorizedStatus';
8 |
9 | function SessionPage() {
10 | const [isModalOpen, setIsModalOpen] = useState(false);
11 |
12 | useUnauthorizedStatus('MAKERS');
13 |
14 | useEffect(() => {
15 | if (isModalOpen) {
16 | document.body.style.overflow = 'hidden';
17 | } else {
18 | document.body.style.overflow = 'unset';
19 | }
20 | return () => {
21 | document.body.style.overflow = 'unset';
22 | };
23 | }, [isModalOpen]);
24 |
25 | return (
26 | <>
27 | {isModalOpen && (
28 |
29 | setIsModalOpen(!isModalOpen)} />
30 |
31 | )}
32 |
33 | 세션 생성하기>}
35 | onClick={() => setIsModalOpen(!isModalOpen)}
36 | />
37 | >
38 | );
39 | }
40 |
41 | export default SessionPage;
42 |
--------------------------------------------------------------------------------
/src/pages/attendanceAdmin/totalScore/index.tsx:
--------------------------------------------------------------------------------
1 | import MemberList from '@/components/attendanceAdmin/totalScore/MemberList';
2 | import { useUnauthorizedStatus } from '@/hooks/useUnauthorizedStatus';
3 |
4 | function TotalScorePage() {
5 | useUnauthorizedStatus('MAKERS');
6 |
7 | return ;
8 | }
9 |
10 | export default TotalScorePage;
11 |
--------------------------------------------------------------------------------
/src/pages/org/org-admin/index.tsx:
--------------------------------------------------------------------------------
1 | import { ToastProvider } from '@sopt-makers/ui';
2 |
3 | import OrgAdmin from '@/components/org/OrgAdmin';
4 |
5 | function OrgAdminPage() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default OrgAdminPage;
14 |
--------------------------------------------------------------------------------
/src/pages/org/recruit-admin/index.tsx:
--------------------------------------------------------------------------------
1 | import RecruitAdmin from '@/components/org/RecruitAdmin';
2 |
3 | function RecruitAdminPage() {
4 | return ;
5 | }
6 |
7 | export default RecruitAdminPage;
8 |
--------------------------------------------------------------------------------
/src/recoil/atom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 | import { recoilPersist } from 'recoil-persist';
3 |
4 | import { ACTIVITY_GENERATION } from '@/utils/generation';
5 |
6 | const sessionStorage =
7 | typeof window !== 'undefined' ? window.sessionStorage : undefined;
8 |
9 | const { persistAtom } = recoilPersist({
10 | key: 'generationStorage',
11 | storage: sessionStorage,
12 | });
13 |
14 | export const currentGenerationState = atom({
15 | key: 'currentGenerationState',
16 | default: ACTIVITY_GENERATION,
17 | effects_UNSTABLE: [persistAtom],
18 | });
19 |
--------------------------------------------------------------------------------
/src/services/api/alarm/index.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios';
2 |
3 | import { client } from '@/services/api/client';
4 |
5 | export interface AlarmListResponse {
6 | alarms: Alarm[];
7 | totalCount: number;
8 | }
9 |
10 | export const postNewAlarm = async (alarmData: PostAlarmData): Promise => {
11 | await client.post('/alarms', alarmData);
12 | };
13 |
14 | export const getAlarmList = async (
15 | generation: number,
16 | status: ALARM_STATUS,
17 | ): Promise => {
18 | const statusQuery = status === 'ALL' ? '' : `&status=${status}`;
19 | const pageQuery = `&size=${100}`; // TODO:: 페이지네이션 적용
20 |
21 | const { data }: AxiosResponse<{ data: AlarmListResponse }> = await client.get(
22 | `/alarms?generation=${generation}${statusQuery}${pageQuery}`,
23 | );
24 |
25 | return data.data;
26 | };
27 |
28 | export const sendAlarm = async (alarmData: AlarmData): Promise => {
29 | const { data }: AxiosResponse = await client.post(
30 | '/alarms/send',
31 | alarmData,
32 | );
33 | return data;
34 | };
35 |
36 | export const createReserveAlarm = async (
37 | alarmData: ReserveAlarmData,
38 | ): Promise => {
39 | const { data }: AxiosResponse = await client.post(
40 | '/alarms/schedule',
41 | alarmData,
42 | );
43 |
44 | return data;
45 | };
46 |
47 | export const getAlarm = async (alarmId: number): Promise => {
48 | const { data }: AxiosResponse<{ data: AlarmDetail }> = await client.get(
49 | `/alarms/${alarmId}`,
50 | );
51 | return data.data;
52 | };
53 |
54 | export const deleteAlarm = async (alarmId: number): Promise => {
55 | await client.delete(`/alarms/${alarmId}`);
56 | };
57 |
--------------------------------------------------------------------------------
/src/services/api/alarm/query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from 'react-query';
2 |
3 | import { AlarmListResponse, getAlarm, getAlarmList } from './index';
4 |
5 | export const useGetAlarmList = (generation: number, status: ALARM_STATUS) => {
6 | return useQuery(
7 | ['alarmList', generation, status],
8 | () => getAlarmList(generation, status),
9 | { staleTime: 10 * 60 * 1000 },
10 | );
11 | };
12 |
13 | export const useGetAlarm = (alarmId: number) => {
14 | return useQuery(
15 | ['alarm', alarmId],
16 | () => getAlarm(alarmId),
17 | { staleTime: 10 * 60 * 1000 },
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/services/api/attendance/index.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios';
2 |
3 | import { client } from '@/services/api/client';
4 |
5 | export const updateMemberAttendStatus = async (
6 | subAttendanceId: number,
7 | status: ATTEND_STATUS,
8 | attribute: SESSION_TYPE,
9 | ): Promise => {
10 | try {
11 | await client.patch('/attendances', { subAttendanceId, status, attribute });
12 | } catch (e) {
13 | console.error(e);
14 | }
15 | };
16 |
17 | export const updateMemberScore = async (
18 | memberId: number,
19 | ): Promise => {
20 | try {
21 | await client.patch(`/attendances/member/${memberId}`, {});
22 | } catch (e) {
23 | console.error(e);
24 | }
25 | };
26 |
27 | export const getMemberAttendance = async (memberId: number) => {
28 | const { data }: AxiosResponse<{ data: ScoreMemberDetail }> = await client.get(
29 | `/attendances/${memberId}`,
30 | );
31 | return data.data;
32 | };
33 |
--------------------------------------------------------------------------------
/src/services/api/attendance/query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from 'react-query';
2 |
3 | import { getMemberAttendance } from './index';
4 |
5 | export const useGetMemberAttendance = (memberId: number) => {
6 | return useQuery(
7 | ['memberAttendance', memberId],
8 | () => getMemberAttendance(memberId),
9 | { staleTime: 10 * 60 * 1000 },
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/src/services/api/auth/index.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosResponse } from 'axios';
2 |
3 | import { client } from '@/services/api/client';
4 | import { setToken } from '@/utils/auth';
5 |
6 | export const userLogin = async (
7 | loginData: LoginData,
8 | ): Promise => {
9 | try {
10 | const { data }: AxiosResponse<{ data: LoginRes }> = await client.post(
11 | '/auth/login',
12 | loginData,
13 | );
14 | const { accessToken, ...user } = data.data;
15 | setToken('ACCESS', accessToken);
16 |
17 | return user;
18 | } catch (e) {
19 | if (e instanceof AxiosError) {
20 | return {
21 | success: false,
22 | message: '아이디 혹은 비밀번호가 일치하지 않아요',
23 | };
24 | } else {
25 | return { success: false, message: '알 수 없는 에러예요' };
26 | }
27 | }
28 | };
29 |
30 | export const reissueAccessToken = async (): Promise => {
31 | try {
32 | const { data }: AxiosResponse<{ data: string }> = await client.patch(
33 | '/auth/refresh',
34 | {},
35 | {
36 | headers: {
37 | 'Reissue-Request': 'true',
38 | },
39 | },
40 | );
41 | const accessToken = data.data;
42 | setToken('ACCESS', accessToken);
43 | } catch (e) {
44 | return { success: false, message: '알 수 없는 에러예요' };
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/src/services/api/banner/index.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios';
2 |
3 | import {
4 | BannerDetailRequest,
5 | BannerDetailResponse,
6 | BannerListResponse,
7 | } from '@/components/bannerAdmin/types/api';
8 | import { DEFAULT_BANNER_LIST_LIMIT, INIT_BANNER_LIST_PAGE } from '@/constants';
9 | import { client } from '@/services/api/client';
10 |
11 | export const postNewBanner = async (bannerData: BannerDetailRequest) => {
12 | const response = await client.post('/banners', bannerData, {
13 | headers: {
14 | 'Content-Type': 'multipart/form-data',
15 | },
16 | });
17 |
18 | return response;
19 | };
20 |
21 | export const deleteBanner = async (bannerId: number) => {
22 | const response = await client.delete(`/banners/${bannerId}`);
23 |
24 | return response;
25 | };
26 |
27 | export const putBanner = async (
28 | bannerId: number,
29 | bannerData: BannerDetailRequest,
30 | ) => {
31 | const response = await client.put(`/banners/${bannerId}`, bannerData, {
32 | headers: {
33 | 'Content-Type': 'multipart/form-data',
34 | },
35 | });
36 |
37 | return response;
38 | };
39 |
40 | export const getBannerDetail = async (bannerId: number) => {
41 | const { data }: AxiosResponse = await client.get(
42 | `/banners/${bannerId}`,
43 | );
44 |
45 | return data;
46 | };
47 |
48 | export const fetchBannerList = async (
49 | status = '',
50 | sort = 'status',
51 | page = INIT_BANNER_LIST_PAGE,
52 | limit = DEFAULT_BANNER_LIST_LIMIT,
53 | ) => {
54 | let queryString = '';
55 |
56 | if (status) {
57 | queryString = `?status=${status}`;
58 | }
59 |
60 | queryString += queryString ? `&sort=${sort}` : `?sort=${sort}`;
61 | queryString += `&page=${page}&limit=${limit}`;
62 |
63 | const { data }: AxiosResponse = await client.get(
64 | `/banners${queryString}`,
65 | );
66 |
67 | return {
68 | banners: data.data.data,
69 | totalCount: data.data.totalCount,
70 | totalPage: data.data.totalPage,
71 | currentPage: data.data.currentPage,
72 | hasNextPage: data.data.hasNextPage,
73 | hasPrevPage: data.data.hasPrevPage,
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/src/services/api/banner/query.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQuery } from 'react-query';
2 |
3 | import { BannerDetailRequest } from '@/components/bannerAdmin/types/api';
4 | import { DEFAULT_BANNER_LIST_LIMIT, INIT_BANNER_LIST_PAGE } from '@/constants';
5 | import {
6 | deleteBanner,
7 | fetchBannerList,
8 | getBannerDetail,
9 | postNewBanner,
10 | putBanner,
11 | } from '@/services/api/banner';
12 |
13 | export const usePostNewBanner = () => {
14 | return useMutation({
15 | mutationFn: (bannerData: BannerDetailRequest) => postNewBanner(bannerData),
16 | });
17 | };
18 |
19 | export const useDeleteBanner = () => {
20 | return useMutation({
21 | mutationFn: (bannerId: number) => deleteBanner(bannerId),
22 | });
23 | };
24 |
25 | export const usePutBanner = () => {
26 | return useMutation({
27 | mutationFn: ({
28 | bannerId,
29 | bannerData,
30 | }: {
31 | bannerId: number;
32 | bannerData: BannerDetailRequest;
33 | }) => putBanner(bannerId, bannerData),
34 | });
35 | };
36 |
37 | export const useGetBannerDetail = (bannerId: number) => {
38 | return useQuery({
39 | queryKey: ['banner', 'detail'],
40 | queryFn: () => getBannerDetail(bannerId),
41 | enabled: bannerId !== 0,
42 | cacheTime: 0,
43 | });
44 | };
45 |
46 | export const useFetchBannerList = (
47 | status = '',
48 | sort = 'status',
49 | page = INIT_BANNER_LIST_PAGE,
50 | limit = DEFAULT_BANNER_LIST_LIMIT,
51 | ) => {
52 | return useQuery(['bannerList', status, sort, page, limit], () =>
53 | fetchBannerList(status, sort, page, limit),
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/services/api/lecture/query.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useInfiniteQuery,
3 | UseInfiniteQueryResult,
4 | useQuery,
5 | } from 'react-query';
6 |
7 | import { PAGE_SIZE } from '@/data/queryData';
8 |
9 | import {
10 | getLectureDetail,
11 | getSessionDetail,
12 | getSessionList,
13 | getSessionMembers,
14 | } from './index';
15 |
16 | export const useGetSessionList = (generation: number, part: string) => {
17 | return useQuery(
18 | ['sessionList', generation, part],
19 | () => getSessionList(generation, part),
20 | { staleTime: 10 * 60 * 1000 },
21 | );
22 | };
23 |
24 | export const useGetLectureDetail = (lectureId: number) => {
25 | return useQuery(
26 | ['lectureDetail', lectureId],
27 | () => getLectureDetail(lectureId),
28 | { staleTime: 10 * 60 * 1000 },
29 | );
30 | };
31 |
32 | export const useGetSessionDetail = (lectureId: number | null) => {
33 | return useQuery(
34 | ['sessionDetail', lectureId],
35 | () => (lectureId ? getSessionDetail(lectureId) : null),
36 | { staleTime: 10 * 60 * 1000, refetchOnReconnect: false },
37 | );
38 | };
39 |
40 | export const useGetInfiniteSessionMembers = (
41 | lectureId: number | null,
42 | part?: PART,
43 | ): UseInfiniteQueryResult<{
44 | attendances: SessionMember[];
45 | totalCount: number;
46 | }> => {
47 | return useInfiniteQuery(
48 | ['sessionMembers', lectureId, part],
49 | async ({ pageParam = 0 }) =>
50 | pageParam !== null &&
51 | (await getSessionMembers(pageParam, lectureId ?? 0, part)),
52 | {
53 | getNextPageParam: (lastPage, pages) =>
54 | lastPage && lastPage.attendances.length < PAGE_SIZE
55 | ? null
56 | : pages.length,
57 | refetchOnReconnect: false,
58 | },
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/services/api/member/index.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios';
2 |
3 | import { client } from '@/services/api/client';
4 | import { buildQuery } from '@/utils';
5 |
6 | export const getMemberList = async (
7 | page: number,
8 | generation: number,
9 | part: PART,
10 | ) => {
11 | const query = buildQuery({
12 | part,
13 | generation: `${generation}`,
14 | page: `${page}`,
15 | });
16 | const {
17 | data,
18 | }: AxiosResponse<{ data: { members: ScoreMember[]; totalCount: number } }> =
19 | await client.get(`/members/list${query}`);
20 | return data.data;
21 | };
22 |
--------------------------------------------------------------------------------
/src/services/api/member/query.ts:
--------------------------------------------------------------------------------
1 | import { useInfiniteQuery, UseInfiniteQueryResult } from 'react-query';
2 |
3 | import { PAGE_SIZE } from '@/data/queryData';
4 |
5 | import { getMemberList } from './index';
6 |
7 | export const useGetInfiniteMemberList = (
8 | generation: number,
9 | part: PART,
10 | ): UseInfiniteQueryResult<{ members: ScoreMember[]; totalCount: number }> => {
11 | return useInfiniteQuery(
12 | ['memberList', generation, part],
13 | async ({ pageParam = 0 }) =>
14 | pageParam !== null && (await getMemberList(pageParam, generation, part)),
15 | {
16 | getNextPageParam: (lastPage, pages) =>
17 | lastPage && lastPage.members.length < PAGE_SIZE ? null : pages.length,
18 | },
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/store/globalStore.ts:
--------------------------------------------------------------------------------
1 | import { atom, DefaultValue, selector } from 'recoil';
2 |
3 | export const user = atom({
4 | key: 'userData',
5 | default: {
6 | id: 0,
7 | adminStatus: 'NOT_CERTIFIED',
8 | name: '',
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/src/styles/mediaQuery.ts:
--------------------------------------------------------------------------------
1 | const bp = {
2 | mobile: 480,
3 | tablet: 1024,
4 | };
5 |
6 | const mq = (label: keyof typeof bp) => {
7 | const bpArray = Object.keys(bp).map((key) => [
8 | key,
9 | bp[key as keyof typeof bp],
10 | ]);
11 |
12 | const [result] = bpArray.reduce((acc, [name, size]) => {
13 | if (label === name) return [...acc, `@media (max-width: ${size}px)`];
14 | return acc;
15 | }, []);
16 |
17 | return result;
18 | };
19 |
20 | export default mq;
21 |
--------------------------------------------------------------------------------
/src/styles/theme.ts:
--------------------------------------------------------------------------------
1 | const theme = {
2 | color: {
3 | main: {
4 | orange50: '#FF6E1D',
5 | blue50: '#346DFF',
6 | purple100: '#8040FF',
7 | purple40: '#C6A9FF',
8 | purpledim100: '#282039',
9 | purpledim20: '#C6A9FF33',
10 | newBlue: '#4B7EFF',
11 | },
12 | sub: {
13 | red: '#E45656',
14 | green: '#378F5C',
15 | yellow: '#D09600',
16 | },
17 | grayscale: {
18 | black100: '#0F1010',
19 | black80: '#1C1D1E',
20 | black60: '#2C2D2E',
21 | black40: '#3C3D40',
22 | realwhite: '#FFFFFF',
23 | white100: '#FCFCFC',
24 | gray10: '#F7F8FA',
25 | gray20: '#EEEFF1',
26 | gray30: '#CED1D2',
27 | gray40: '#C0C5C9',
28 | gray60: '#989BA0',
29 | gray80: '#808388',
30 | gray100: '#606265',
31 | },
32 | },
33 | };
34 |
35 | export default theme;
36 |
--------------------------------------------------------------------------------
/src/utils/date.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 |
3 | type FormatType = 'date' | 'time';
4 |
5 | export const transDate = (
6 | date: string | undefined,
7 | type: FormatType,
8 | ): string => {
9 | const dayjsTranslator = dayjs(date);
10 | switch (type) {
11 | case 'date':
12 | return dayjsTranslator.format('YYYY/MM/DD');
13 | case 'time':
14 | return dayjsTranslator.format('HH:mm');
15 | default:
16 | throw new Error(`Unsupported type: ${type}`);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/generation.ts:
--------------------------------------------------------------------------------
1 | export const ACTIVITY_GENERATION: string = '36';
2 |
3 | export const GENERATION_LIST: string[] = ['36', '35', '34', '33', '32'];
4 |
--------------------------------------------------------------------------------
/src/utils/nav.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IcAlarmMenu,
3 | IcAttendanceMenu,
4 | IcBannerMenu,
5 | IcOrgMenu,
6 | } from '@/assets/icons';
7 |
8 | export const GENERATION_INFO = [
9 | { generation: '36', slogan: 'AT' },
10 | {
11 | generation: '35',
12 | slogan: 'AND',
13 | },
14 | {
15 | generation: '34',
16 | slogan: 'NOW',
17 | },
18 | {
19 | generation: '33',
20 | slogan: 'DO',
21 | },
22 | {
23 | generation: '32',
24 | slogan: 'GO',
25 | },
26 | ];
27 |
28 | export const MENU_LIST = [
29 | {
30 | title: '출석 관리',
31 | MenuIcon: IcAttendanceMenu,
32 | subMenu: ['출석 세션', '출석 총점'],
33 | path: ['/attendanceAdmin/session', '/attendanceAdmin/totalScore'],
34 | },
35 | {
36 | title: '공홈 관리',
37 | MenuIcon: IcOrgMenu,
38 | subMenu: ['공식홈페이지', '지원서'],
39 | path: ['/org/org-admin', '/org/recruit-admin'],
40 | },
41 | {
42 | title: '알림 관리',
43 | MenuIcon: IcAlarmMenu,
44 | path: ['/alarmAdmin'],
45 | },
46 | {
47 | title: '배너 관리',
48 | MenuIcon: IcBannerMenu,
49 | path: ['/bannerAdmin'],
50 | },
51 | ];
52 |
--------------------------------------------------------------------------------
/src/utils/putObject.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosResponse } from 'axios';
2 |
3 | import { orgClient } from '@/services/api/client';
4 | import { getBearerTokenAuthHeader } from '@/utils/auth';
5 |
6 | export const putObject = async (file: File): Promise => {
7 | const REMOVE_QUERY_STRING_REGEX = /\?.*/;
8 |
9 | const extension = file.name.split('.').pop();
10 |
11 | const { data }: AxiosResponse = await orgClient.get(
12 | `/file/presigned-url?fileType=${extension}`,
13 | {
14 | headers: { ...getBearerTokenAuthHeader() },
15 | },
16 | );
17 |
18 | if (data) {
19 | try {
20 | const res = await axios.put(data.presignedUrl, file);
21 | if (res.status !== 200) {
22 | return null;
23 | }
24 | return data.presignedUrl.replace(REMOVE_QUERY_STRING_REGEX, '');
25 | } catch (e) {
26 | return null;
27 | }
28 | }
29 | return null;
30 | };
31 |
--------------------------------------------------------------------------------
/src/utils/session.ts:
--------------------------------------------------------------------------------
1 | export const attributeList: string[] = ['세미나', '행사', '기타'];
2 |
3 | export const sessionTranslator: Record = {
4 | 세미나: 'SEMINAR',
5 | 행사: 'EVENT',
6 | 기타: 'ETC',
7 | };
8 |
9 | export const partList = [
10 | '전체',
11 | '기획',
12 | '디자인',
13 | '서버',
14 | 'iOS',
15 | '안드로이드',
16 | '웹',
17 | ];
18 |
19 | export const partTranslator: Record = {
20 | 전체: 'ALL',
21 | 기획: 'PLAN',
22 | 디자인: 'DESIGN',
23 | 서버: 'SERVER',
24 | iOS: 'IOS',
25 | 안드로이드: 'ANDROID',
26 | 웹: 'WEB',
27 | };
28 |
29 | export const getPartValue = (obj: Record, value: string) => {
30 | return Object.keys(obj).find((key) => obj[key] === value);
31 | };
32 |
33 | export const times: string[] = [];
34 |
35 | for (let i = 0; i < 24; i++) {
36 | const hour = i < 10 ? `0${i}` : `${i}`;
37 | times.push(`${hour}:00`);
38 | times.push(`${hour}:30`);
39 | }
40 |
--------------------------------------------------------------------------------
/src/utils/zIndex.ts:
--------------------------------------------------------------------------------
1 | const zIndex = {
2 | nav: 10,
3 | header: 10,
4 | footer: 10,
5 | helper: 11,
6 | select: 99,
7 | dim: 15,
8 | modal: 20,
9 | };
10 |
11 | export default zIndex;
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "jsxImportSource": "@emotion/react",
17 | "incremental": true,
18 | "types": ["@emotion/react/types/css-prop", "@types/jest"],
19 | // "typeRoots": ["./node_modules/@types", "./src/types/global.d.ts"],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
25 | "exclude": ["node_modules", "functions/**/*"]
26 | }
27 |
--------------------------------------------------------------------------------