diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..ad2db98
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,3 @@
+VITE_BASE_URL=/
+VITE_GOOGLE_AUTH_CLIENT_ID=268837811477-28b8i24r1sb0aroltho84ia6jecj74h7.apps.googleusercontent.com
+VITE_GOOGLE_AUTH_REDIRECT_URI=http://localhost:5173/loading
\ No newline at end of file
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
new file mode 100644
index 0000000..1d4852e
--- /dev/null
+++ b/.github/workflows/lint.yaml
@@ -0,0 +1,43 @@
+name: Lint and Type Check
+
+on:
+ pull_request:
+ # 모든 브랜치에 대해 PR 시 실행
+ branches:
+ - '**'
+ push:
+ # 모든 브랜치에 대해 푸시 시 실행
+ branches:
+ - '**'
+
+jobs:
+ lint:
+ name: Lint and Type Check
+ runs-on: ubuntu-latest
+
+ steps:
+ # Checkout the repository
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Set up Node.js environment
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '18'
+
+ # Install dependencies
+ - name: Install dependencies
+ run: npm ci
+
+ # Run ESLint to check for linting errors
+ - name: Run ESLint
+ run: npm run lint
+
+ # Run Prettier to check formatting
+ - name: Run Prettier Check
+ run: npm run format -- --check
+
+ # Run TypeScript compiler to check for type errors
+ - name: Run TypeScript Check
+ run: npm run tsc
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 7ea2fad..477112a 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,6 +1,11 @@
import React from 'react';
import type { Preview } from '@storybook/react';
import AppProviders from '../src/components/providers/index.provider';
+import { initialize, mswLoader } from 'msw-storybook-addon';
+import { handlers } from '../src/mocks/handlers';
+import { MemoryRouter } from 'react-router-dom';
+
+initialize();
const preview: Preview = {
parameters: {
@@ -10,15 +15,21 @@ const preview: Preview = {
date: /Date$/i,
},
},
+ msw: {
+ handlers: [...handlers],
+ },
},
tags: ['autodocs'],
decorators: [
(Story) => (
-
-
-
+
+
+
+
+
),
],
+ loaders: [mswLoader],
};
export default preview;
diff --git a/README.md b/README.md
index 9c670e9..0b3ddf6 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,42 @@
-# 🍪 내가 먹은 쿠키 - 18조 FE
\ No newline at end of file
+# 🍪 내가 먹은 쿠키 - 18조 FE
+
+## 🙋♂️ 6주차 코드리뷰 질문
+- 하나의 파일 내에서 함수 표현식과 선언식을 같이 사용해도 되는지 궁금합니다.
+- 기존 Weekly 브랜치에 components 경로에 `RoleModal` 컴포넌트가 있다고 가정하면, 해당 Weekly브랜치를 통해 새로 분기된 Feat 브랜치에 기존 components에 `RoleModal` 컴포넌트를 features 경로에 옮기고 두 브랜치를 머지하게되면 두개의 파일이 생기게 됩니다. 이를 해결하려면 어떻게 해야하나요?
+
+## 🙋♂️ 5주차 코드리뷰 질문
+- 하나의 페이지 내에서만 여러번 사용되는 공통 컴포넌트의 경우, components/common 폴더에 공통 컴포넌트로 만들어 취급하는 것이 좋은지, 혹은 해당 페이지 코드 파일이 위치한 폴더에 컴포넌트를 만들거나 해당 페이지 코드 파일 하단에 작성하는 등 colocation 원칙을 적용해서 가까이 위치시키는 것이 좋을지 궁금합니다.
+- `Header` 컴포넌트에서 다른 theme을 가진 버튼들에 공통된 스타일을 적용하면서, 특정 버튼에만 추가적인 스타일을 주는 작업을 했습니다. 아래와 같이 각 버튼에 공통적으로 적용될 스타일을 `commonButtonStyles`로 정의하고, `theme=default`인 버튼에만 추가 스타일을 적용해보았는데, 제가 구현한 방식보다 더 괜찮은 방법이 있는지 궁금합니다.
+```jsx
+const commonButtonStyles = css`
+ white-space: nowrap;
+ border-radius: 4px;
+`;
+```
+```
+
+
+
+```
+- 태블릿(`768px`)과 모바일(`480px`)에서 반응형을 고려하여 `breakpoints`를 정의하였고, 이를 보다 명시적으로 활용하기 위해 `responsiveStyles` 함수를 구현했습니다.
+멘토님께서는 보통 반응형 스타일링을 구현할 때 어떤 방식을 사용하시나요?
+혹시 제가 사용한 `responsiveStyles` 함수보다 효율적이거나 코드의 가독성을 높일 수 있는 더 나은 방법이 있을까요? 멘토님이 추천하는 방법이나 일반적으로 사용되는 best practice 또한 궁금합니다.
+- 현재 `Modal` 컴포넌트를 사용할 때마다 `useToggle` 커스텀 훅을 함께 사용해야 해서, 모달을 제어하기 위한 코드가 흩어져 있는 느낌입니다. 이렇게 되면 모달 관련 로직으로 인해서 단일 책임 원칙에 어긋난다는 생각이 들곤 합니다.
+보다 나은 방식으로 `Modal` 컴포넌트를 동작시킬 수 있는 방법이 있을까요? `useToggle`처럼 모달을 제어하는 로직을 간소화하고, 모달 컴포넌트 자체가 스스로 상태를 관리하거나 쉽게 제어 가능한 형태로 구현할 수 있는지 궁금합니다.
+
+## 🙋♂️ 4주차 코드리뷰 질문
+- `Modal` 컴포넌트를 구현하면서 텍스트 부분과 버튼 부분에 들어갈 내용은 개발할 때 코드를 작성하는 사람이 자유롭게 작성하여 구성할 수 있도록 `textChildren`과 `buttonChildren`만으로 구성하였는데, 더 적합하거나 지향하는 방식이 있을까요
diff --git a/eslint.config.js b/eslint.config.js
index e9c92e0..94efdac 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -21,6 +21,8 @@ export default tseslint.config(
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'react-refresh/only-export-components': 'off',
+ 'react-hooks/rules-of-hooks': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
},
},
);
diff --git a/package-lock.json b/package-lock.json
index 462ba8b..6d70f51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,10 +12,14 @@
"@emotion/css": "^11.13.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
+ "@loadable/component": "^5.16.4",
+ "@react-oauth/google": "^0.12.1",
"@tanstack/react-query": "^5.56.2",
+ "axios": "^1.7.7",
"csstype": "^3.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-error-boundary": "^4.1.2",
"react-hook-form": "^7.53.0",
"react-router-dom": "^6.26.2",
"zustand": "^4.5.5"
@@ -31,6 +35,7 @@
"@storybook/react": "^8.3.0",
"@storybook/react-vite": "^8.3.0",
"@storybook/test": "^8.3.0",
+ "@types/loadable__component": "^5.13.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-slick": "^0.23.13",
@@ -46,6 +51,7 @@
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"msw": "^2.4.6",
+ "msw-storybook-addon": "^2.0.3",
"prettier": "3.3.3",
"react-slick": "^0.30.2",
"slick-carousel": "^1.8.1",
@@ -1351,6 +1357,26 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@loadable/component": {
+ "version": "5.16.4",
+ "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.16.4.tgz",
+ "integrity": "sha512-fJWxx9b5WHX90QKmizo9B+es2so8DnBthI1mbflwCoOyvzEwxiZ/SVDCTtXEnHG72/kGBdzr297SSIekYtzSOQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.18",
+ "hoist-non-react-statics": "^3.3.1",
+ "react-is": "^16.12.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "react": "^16.3.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@mdx-js/react": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz",
@@ -1442,6 +1468,15 @@
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
"dev": true
},
+ "node_modules/@react-oauth/google": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz",
+ "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
@@ -2955,6 +2990,15 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
+ "node_modules/@types/loadable__component": {
+ "version": "5.13.9",
+ "resolved": "https://registry.npmjs.org/@types/loadable__component/-/loadable__component-5.13.9.tgz",
+ "integrity": "sha512-QWOtIkwZqHNdQj3nixQ8oyihQiTMKZLk/DNuvNxMSbTfxf47w+kqcbnxlUeBgAxdOtW0Dh48dTAIp83iJKtnrQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/lodash": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
@@ -3600,6 +3644,11 @@
"node": ">=4"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -3615,6 +3664,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axios": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+ "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -4033,6 +4092,17 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -4192,6 +4262,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -5185,6 +5263,25 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -5194,6 +5291,19 @@
"is-callable": "^1.1.3"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -6370,7 +6480,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -6379,7 +6488,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -6489,6 +6597,18 @@
}
}
},
+ "node_modules/msw-storybook-addon": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.3.tgz",
+ "integrity": "sha512-CzHmGO32JeOPnyUnRWnB0PFTXCY1HKfHiEB/6fYoUYiFm2NYosLjzs9aBd3XJUryYEN0avJqMNh7nCRDxE5JjQ==",
+ "dev": true,
+ "dependencies": {
+ "is-node-process": "^1.0.1"
+ },
+ "peerDependencies": {
+ "msw": "^2.0.0"
+ }
+ },
"node_modules/msw/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -7086,6 +7206,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -7265,6 +7390,17 @@
"integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
"dev": true
},
+ "node_modules/react-error-boundary": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz",
+ "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-hook-form": {
"version": "7.53.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
diff --git a/package.json b/package.json
index 4f9a70b..a5a4db4 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
},
"lint-staged": {
"**/*.{tsx,ts,jsx,js}": [
+ "bash -c tsc -p tsconfig.json --noEmit",
"eslint --fix --cache",
"prettier --write --cache"
]
@@ -27,10 +28,14 @@
"@emotion/css": "^11.13.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
+ "@loadable/component": "^5.16.4",
+ "@react-oauth/google": "^0.12.1",
"@tanstack/react-query": "^5.56.2",
+ "axios": "^1.7.7",
"csstype": "^3.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-error-boundary": "^4.1.2",
"react-hook-form": "^7.53.0",
"react-router-dom": "^6.26.2",
"zustand": "^4.5.5"
@@ -46,6 +51,7 @@
"@storybook/react": "^8.3.0",
"@storybook/react-vite": "^8.3.0",
"@storybook/test": "^8.3.0",
+ "@types/loadable__component": "^5.13.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-slick": "^0.23.13",
@@ -61,6 +67,7 @@
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"msw": "^2.4.6",
+ "msw-storybook-addon": "^2.0.3",
"prettier": "3.3.3",
"react-slick": "^0.30.2",
"slick-carousel": "^1.8.1",
@@ -74,5 +81,10 @@
"extends": [
"plugin:storybook/recommended"
]
+ },
+ "msw": {
+ "workerDirectory": [
+ "public"
+ ]
}
}
diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js
new file mode 100644
index 0000000..0eeeb55
--- /dev/null
+++ b/public/mockServiceWorker.js
@@ -0,0 +1,280 @@
+/* tslint:disable */
+
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const PACKAGE_VERSION = '2.4.6';
+const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423';
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
+const activeClientIds = new Set();
+
+self.addEventListener('install', function () {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id;
+
+ if (!clientId || !self.clients) {
+ return;
+ }
+
+ const client = await self.clients.get(clientId);
+
+ if (!client) {
+ return;
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ });
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ });
+ break;
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: {
+ packageVersion: PACKAGE_VERSION,
+ checksum: INTEGRITY_CHECKSUM,
+ },
+ });
+ break;
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId);
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: true,
+ });
+ break;
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId);
+ break;
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId);
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId;
+ });
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister();
+ }
+
+ break;
+ }
+ }
+});
+
+self.addEventListener('fetch', function (event) {
+ const { request } = event;
+
+ // Bypass navigation requests.
+ if (request.mode === 'navigate') {
+ return;
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return;
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return;
+ }
+
+ // Generate unique request ID.
+ const requestId = crypto.randomUUID();
+ event.respondWith(handleRequest(event, requestId));
+});
+
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event);
+ const response = await getResponse(event, client, requestId);
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ (async function () {
+ const responseClone = response.clone();
+
+ sendToClient(
+ client,
+ {
+ type: 'RESPONSE',
+ payload: {
+ requestId,
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
+ type: responseClone.type,
+ status: responseClone.status,
+ statusText: responseClone.statusText,
+ body: responseClone.body,
+ headers: Object.fromEntries(responseClone.headers.entries()),
+ },
+ },
+ [responseClone.body],
+ );
+ })();
+ }
+
+ return response;
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId);
+
+ if (client?.frameType === 'top-level') {
+ return client;
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ });
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible';
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id);
+ });
+}
+
+async function getResponse(event, client, requestId) {
+ const { request } = event;
+
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const requestClone = request.clone();
+
+ function passthrough() {
+ const headers = Object.fromEntries(requestClone.headers.entries());
+
+ // Remove internal MSW request header so the passthrough request
+ // complies with any potential CORS preflight checks on the server.
+ // Some servers forbid unknown request headers.
+ delete headers['x-msw-intention'];
+
+ return fetch(requestClone, { headers });
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough();
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough();
+ }
+
+ // Notify the client that a request has been intercepted.
+ const requestBuffer = await request.arrayBuffer();
+ const clientMessage = await sendToClient(
+ client,
+ {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ url: request.url,
+ mode: request.mode,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: requestBuffer,
+ keepalive: request.keepalive,
+ },
+ },
+ [requestBuffer],
+ );
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data);
+ }
+
+ case 'PASSTHROUGH': {
+ return passthrough();
+ }
+ }
+
+ return passthrough();
+}
+
+function sendToClient(client, message, transferrables = []) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error);
+ }
+
+ resolve(event.data);
+ };
+
+ client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean)));
+ });
+}
+
+async function respondWithMock(response) {
+ // Setting response status code to 0 is a no-op.
+ // However, when responding with a "Response.error()", the produced Response
+ // instance will have status code set to 0. Since it's not possible to create
+ // a Response instance with status code 0, handle that use-case separately.
+ if (response.status === 0) {
+ return Response.error();
+ }
+
+ const mockedResponse = new Response(response.body, response);
+
+ Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
+ value: true,
+ enumerable: true,
+ });
+
+ return mockedResponse;
+}
diff --git a/src/apis/apiPath.ts b/src/apis/apiPath.ts
new file mode 100644
index 0000000..e4fc864
--- /dev/null
+++ b/src/apis/apiPath.ts
@@ -0,0 +1,11 @@
+export const APIPath = {
+ postNotice: '/api/recruitments',
+ allApplication: '/api/application/all',
+ signEmployeeContract: '/api/contract',
+ makeEmployerContract: '/api/categories',
+ downloadContract: '/api/contract/:applyId/download',
+};
+
+export const getDynamicAPIPath = {
+ downloadContract: (applyId: number) => APIPath.downloadContract.replace(':applyId', applyId.toString()),
+};
diff --git a/src/apis/auth/hooks/useAuthFetch.ts b/src/apis/auth/hooks/useAuthFetch.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/apis/auth/mock/index.mock.ts b/src/apis/auth/mock/index.mock.ts
deleted file mode 100644
index 056a6e2..0000000
--- a/src/apis/auth/mock/index.mock.ts
+++ /dev/null
@@ -1 +0,0 @@
-// 데이터 관련된
diff --git a/src/apis/auth/mutations/useGoogleOAuth.tsx b/src/apis/auth/mutations/useGoogleOAuth.tsx
new file mode 100644
index 0000000..63ae45b
--- /dev/null
+++ b/src/apis/auth/mutations/useGoogleOAuth.tsx
@@ -0,0 +1,81 @@
+import { clientInstance } from '@apis/instance';
+import { useMutation } from '@tanstack/react-query';
+import { AxiosError } from 'axios';
+import { AUTH_PATH } from '../path';
+import { OAuthRequest } from '../types/request';
+import { OAuthResponse } from '../types/response';
+import { useNavigate } from 'react-router-dom';
+import { useCallback, useState, useEffect } from 'react';
+import ROUTE_PATH from '@/routes/path';
+
+const clientId = import.meta.env.VITE_GOOGLE_AUTH_CLIENT_ID;
+const redirectUri = import.meta.env.VITE_GOOGLE_AUTH_REDIRECT_URI;
+
+const getAccessTokenFromUrl = () => {
+ const hashParams = new URLSearchParams(window.location.hash.substring(1));
+ return hashParams.get('access_token');
+};
+
+const postOAuth = async ({ token }: OAuthRequest): Promise => {
+ const res = await clientInstance.post(AUTH_PATH.OAUTH, { token });
+
+ const accessToken = res.headers['authorization'];
+ if (!accessToken) {
+ throw new Error('Authorization header is missing in the response');
+ }
+
+ return {
+ accessToken,
+ type: res.data.type,
+ profileImage: res.data.profileImage,
+ };
+};
+
+export function useGoogleOAuth(): {
+ isLoading: boolean;
+ redirectToGoogleLogin: () => void;
+} {
+ const [isLoading, setIsLoading] = useState(false);
+ const navigate = useNavigate();
+
+ const redirectToGoogleLogin = useCallback(() => {
+ const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly&include_granted_scopes=true&response_type=token&state=state_parameter_passthrough_value&redirect_uri=${redirectUri}&client_id=${clientId}`;
+
+ window.location.href = googleAuthUrl;
+ }, []);
+
+ const { mutate: handleLoginPost, status } = useMutation({
+ mutationFn: postOAuth,
+ onSuccess: (data) => {
+ const { accessToken, type } = data;
+
+ localStorage.setItem('token', accessToken);
+
+ if (type === 'first') {
+ navigate(ROUTE_PATH.AUTH.SIGN_UP);
+ } else {
+ navigate(ROUTE_PATH.HOME);
+ }
+
+ window.location.reload();
+ },
+ onError: (error) => {
+ console.error('Error during login:', error);
+ setIsLoading(false);
+ },
+ });
+
+ const isMutating = status === 'pending';
+
+ useEffect(() => {
+ const token = getAccessTokenFromUrl();
+ if (token) {
+ setIsLoading(true);
+ handleLoginPost({ token });
+ } else {
+ console.log('로그인 재시도하세요.');
+ }
+ }, [handleLoginPost]);
+
+ return { isLoading: isLoading || isMutating, redirectToGoogleLogin };
+}
diff --git a/src/apis/auth/mutations/useRegister.tsx b/src/apis/auth/mutations/useRegister.tsx
new file mode 100644
index 0000000..e3ad42e
--- /dev/null
+++ b/src/apis/auth/mutations/useRegister.tsx
@@ -0,0 +1,17 @@
+import { clientInstance } from '@apis/instance';
+import { useMutation, UseMutationResult } from '@tanstack/react-query';
+import { AxiosError } from 'axios';
+import { AUTH_PATH } from '../path';
+import { RegisterRequest } from '../types/request';
+import { RegisterResponse } from '../types/response';
+
+const postRegister = async ({ type }: RegisterRequest): Promise => {
+ const res = await clientInstance.post(AUTH_PATH.REGISTER, { type });
+ return res.data;
+};
+
+export const useRegister = (): UseMutationResult => {
+ return useMutation({
+ mutationFn: postRegister,
+ });
+};
diff --git a/src/apis/auth/path.ts b/src/apis/auth/path.ts
new file mode 100644
index 0000000..4205fc1
--- /dev/null
+++ b/src/apis/auth/path.ts
@@ -0,0 +1,6 @@
+const BASE_URL = '/api';
+
+export const AUTH_PATH = {
+ OAUTH: `${BASE_URL}/oauth`,
+ REGISTER: `${BASE_URL}/register`,
+};
diff --git a/src/apis/auth/types/request.ts b/src/apis/auth/types/request.ts
new file mode 100644
index 0000000..86161ab
--- /dev/null
+++ b/src/apis/auth/types/request.ts
@@ -0,0 +1,7 @@
+export interface OAuthRequest {
+ token: string;
+}
+
+export interface RegisterRequest {
+ type: 'employee' | 'employer';
+}
diff --git a/src/apis/auth/types/response.ts b/src/apis/auth/types/response.ts
new file mode 100644
index 0000000..68d4e49
--- /dev/null
+++ b/src/apis/auth/types/response.ts
@@ -0,0 +1,9 @@
+export interface OAuthResponse {
+ accessToken: string;
+ type: 'first' | 'employee' | 'employer';
+ profileImage: string;
+}
+
+export interface RegisterResponse {
+ status: boolean;
+}
diff --git a/src/apis/employee/hooks/useGetMyApplication.ts b/src/apis/employee/hooks/useGetMyApplication.ts
new file mode 100644
index 0000000..d19caa5
--- /dev/null
+++ b/src/apis/employee/hooks/useGetMyApplication.ts
@@ -0,0 +1,18 @@
+import { APIPath } from '@/apis/apiPath';
+import { clientInstance } from '@/apis/instance';
+import { useQuery } from '@tanstack/react-query';
+
+export const getMyApplicationPath = () => `${APIPath.allApplication}`;
+
+const myApplicationQueryKey = [getMyApplicationPath()];
+
+export const getMyApplication = async () => {
+ const response = await clientInstance.get(getMyApplicationPath());
+ return response.data;
+};
+
+export const useGetMyApplication = () =>
+ useQuery({
+ queryKey: myApplicationQueryKey,
+ queryFn: getMyApplication,
+ });
diff --git a/src/apis/employee/mock/getMyApplication.mock.ts b/src/apis/employee/mock/getMyApplication.mock.ts
new file mode 100644
index 0000000..4217cbf
--- /dev/null
+++ b/src/apis/employee/mock/getMyApplication.mock.ts
@@ -0,0 +1,9 @@
+import { http, HttpResponse } from 'msw';
+import { getMyApplicationPath } from '../hooks/useGetMyApplication';
+import { myRecruitList } from '@/pages/employee/myPage/data/index.mock';
+
+export const EmployeePageMockHandler = [
+ http.get(getMyApplicationPath(), () => {
+ return HttpResponse.json(myRecruitList);
+ }),
+];
diff --git a/src/apis/employer/hooks/usePostNotice.ts b/src/apis/employer/hooks/usePostNotice.ts
new file mode 100644
index 0000000..b54af4a
--- /dev/null
+++ b/src/apis/employer/hooks/usePostNotice.ts
@@ -0,0 +1,32 @@
+import { APIPath } from '@/apis/apiPath';
+import { clientInstance } from '@/apis/instance';
+import { useMutation } from '@tanstack/react-query';
+
+export type NoticeRequestData = {
+ title?: string;
+ companyScale?: string;
+ area?: string;
+ salary?: string;
+ workDuration?: string;
+ workDays?: string;
+ workType?: string;
+ workHours?: string;
+ requestedCareer?: string;
+ majorBusiness?: string;
+ eligibilityCriteria?: string;
+ preferredConditions?: string;
+ employerName?: string;
+ companyName?: string;
+};
+
+export const getPostNoticePath = () => `${APIPath.postNotice}`;
+
+export const postNotice = async (req: NoticeRequestData) => {
+ const response = await clientInstance.post(getPostNoticePath(), req);
+ return response.data;
+};
+
+export const FetchPostNotice = () =>
+ useMutation({
+ mutationFn: postNotice,
+ });
diff --git a/src/apis/employer/mock/postNotice.mock.ts b/src/apis/employer/mock/postNotice.mock.ts
new file mode 100644
index 0000000..40e2a8b
--- /dev/null
+++ b/src/apis/employer/mock/postNotice.mock.ts
@@ -0,0 +1,10 @@
+import { http, HttpResponse } from 'msw';
+import { getPostNoticePath } from '../hooks/usePostNotice';
+
+export const noticesMockHandler = [
+ http.post(getPostNoticePath(), async ({ request }) => {
+ // 가로챈 요청 바디를 JSON으로 읽기
+ const req = await request.json();
+ return HttpResponse.json(req, { status: 201 });
+ }),
+];
diff --git a/src/pages/home/data/index.mock.ts b/src/apis/home/mocks/recruitmentsMockHandler.ts
similarity index 71%
rename from src/pages/home/data/index.mock.ts
rename to src/apis/home/mocks/recruitmentsMockHandler.ts
index 17da861..8243ff8 100644
--- a/src/pages/home/data/index.mock.ts
+++ b/src/apis/home/mocks/recruitmentsMockHandler.ts
@@ -1,22 +1,14 @@
-export const images = [
- {
- id: 1,
- imageUrl: 'https://www.v-on.kr/wp-content/uploads/2022/06/IT_twi001t2302755-1024x683.jpg',
- },
- {
- id: 2,
- imageUrl: 'https://www.v-on.kr/wp-content/uploads/2022/06/IT_twi001t2302755-1024x683.jpg',
- },
- {
- id: 3,
- imageUrl: 'https://www.v-on.kr/wp-content/uploads/2022/06/IT_twi001t2302755-1024x683.jpg',
- },
+import { http, HttpResponse } from 'msw';
+import { HOME_PATH } from '../path';
+
+export const recruitmentsMockHandler = [
+ http.get(HOME_PATH.RECRUITMENTS, () => HttpResponse.json(RECRUITMENTS_RESPONSE_DATA)),
];
-export const recruitmentList = [
+const RECRUITMENTS_RESPONSE_DATA = [
{
recruitmentId: 1,
- image:
+ imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '제목',
vietnameseTitle: '제목',
@@ -27,7 +19,7 @@ export const recruitmentList = [
},
{
recruitmentId: 2,
- image:
+ imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '제목',
vietnameseTitle: '제목',
@@ -38,7 +30,7 @@ export const recruitmentList = [
},
{
recruitmentId: 3,
- image:
+ imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '제목',
vietnameseTitle: '제목',
diff --git a/src/apis/home/mocks/slidesMockHandler.ts b/src/apis/home/mocks/slidesMockHandler.ts
new file mode 100644
index 0000000..1bf8c26
--- /dev/null
+++ b/src/apis/home/mocks/slidesMockHandler.ts
@@ -0,0 +1,19 @@
+import { http, HttpResponse } from 'msw';
+import { HOME_PATH } from '../path';
+
+export const slidesMockHandler = [http.get(HOME_PATH.SLIDER, () => HttpResponse.json(SLIDER_RESPONSE_DATA))];
+
+const SLIDER_RESPONSE_DATA = [
+ {
+ id: 1,
+ imageUrl: 'https://www.v-on.kr/wp-content/uploads/2022/06/IT_twi001t2302755-1024x683.jpg',
+ },
+ {
+ id: 2,
+ imageUrl: 'https://www.v-on.kr/wp-content/uploads/2022/06/IT_twi001t2302755-1024x683.jpg',
+ },
+ {
+ id: 3,
+ imageUrl: 'https://www.v-on.kr/wp-content/uploads/2022/06/IT_twi001t2302755-1024x683.jpg',
+ },
+];
diff --git a/src/apis/home/path.ts b/src/apis/home/path.ts
new file mode 100644
index 0000000..74ad5b1
--- /dev/null
+++ b/src/apis/home/path.ts
@@ -0,0 +1,6 @@
+const BASE_URL = '/api';
+
+export const HOME_PATH = {
+ RECRUITMENTS: `${BASE_URL}/recruitments`,
+ SLIDER: `${BASE_URL}/slides`,
+};
diff --git a/src/apis/home/queries/queryKeys.ts b/src/apis/home/queries/queryKeys.ts
new file mode 100644
index 0000000..ffb872c
--- /dev/null
+++ b/src/apis/home/queries/queryKeys.ts
@@ -0,0 +1,4 @@
+export const QUERY_KEYS = {
+ RECRUITMENTS: 'recruitments',
+ SLIDES: 'slides',
+};
diff --git a/src/apis/home/queries/useFetchRecruitments.tsx b/src/apis/home/queries/useFetchRecruitments.tsx
new file mode 100644
index 0000000..7bb8bb8
--- /dev/null
+++ b/src/apis/home/queries/useFetchRecruitments.tsx
@@ -0,0 +1,18 @@
+import { AxiosError } from 'axios';
+import { RecruitmentResponse } from '../types/response';
+import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
+import { clientInstance } from '@apis/instance';
+import { HOME_PATH } from '../path';
+import { QUERY_KEYS } from './queryKeys';
+
+const getRecruitments = async (): Promise => {
+ const res = await clientInstance.get(HOME_PATH.RECRUITMENTS);
+ return res.data;
+};
+
+export const useFetchRecruitments = (): UseSuspenseQueryResult => {
+ return useSuspenseQuery({
+ queryKey: [QUERY_KEYS.RECRUITMENTS],
+ queryFn: () => getRecruitments(),
+ });
+};
diff --git a/src/apis/home/queries/useFetchSlides.tsx b/src/apis/home/queries/useFetchSlides.tsx
new file mode 100644
index 0000000..adfcba0
--- /dev/null
+++ b/src/apis/home/queries/useFetchSlides.tsx
@@ -0,0 +1,18 @@
+import { AxiosError } from 'axios';
+import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
+import { clientInstance } from '@apis/instance';
+import { HOME_PATH } from '../path';
+import { SlidesResponse } from '../types/response';
+import { QUERY_KEYS } from './queryKeys';
+
+const getSlides = async (): Promise => {
+ const res = await clientInstance.get(HOME_PATH.SLIDER);
+ return res.data;
+};
+
+export const useFetchSlides = (): UseSuspenseQueryResult => {
+ return useSuspenseQuery({
+ queryKey: [QUERY_KEYS.SLIDES],
+ queryFn: () => getSlides(),
+ });
+};
diff --git a/src/apis/home/types/response.ts b/src/apis/home/types/response.ts
new file mode 100644
index 0000000..8b6990a
--- /dev/null
+++ b/src/apis/home/types/response.ts
@@ -0,0 +1,15 @@
+export type RecruitmentResponse = {
+ recruitmentId: number;
+ imageUrl: string;
+ koreanTitle: string;
+ vietnameseTitle: string;
+ companyName: string;
+ salary: number;
+ workHours: string;
+ area: string;
+};
+
+export type SlidesResponse = {
+ id: number;
+ imageUrl: string;
+};
diff --git a/src/apis/instance.ts b/src/apis/instance.ts
index e69de29..8258c92 100644
--- a/src/apis/instance.ts
+++ b/src/apis/instance.ts
@@ -0,0 +1,48 @@
+import axios, { AxiosError, AxiosInstance } from 'axios';
+import { QueryClient } from '@tanstack/react-query';
+
+const BASE_URL = import.meta.env.VITE_BASE_URL;
+
+const setInterceptors = (instance: AxiosInstance) => {
+ instance.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ console.log('interceptor > error', error);
+ return Promise.reject(error);
+ },
+ );
+
+ instance.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token');
+
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+
+ return config;
+ },
+ (error: AxiosError) => {
+ console.log('interceptor > error', error);
+ Promise.reject(error);
+ },
+ );
+};
+
+const createInstance = () => {
+ const instance = axios.create({
+ baseURL: BASE_URL,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ setInterceptors(instance);
+
+ return instance;
+};
+
+export const queryClient = new QueryClient();
+
+export const clientInstance = createInstance();
diff --git a/src/assets/icons/arrow/big-right-blue.svg b/src/assets/icons/arrow/big-right-blue.svg
new file mode 100644
index 0000000..6fdba45
--- /dev/null
+++ b/src/assets/icons/arrow/big-right-blue.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/arrow/down-blue.svg b/src/assets/icons/arrow/down-blue.svg
new file mode 100644
index 0000000..4d0f94d
--- /dev/null
+++ b/src/assets/icons/arrow/down-blue.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/employeePage/bag.svg b/src/assets/icons/employeePage/bag.svg
new file mode 100644
index 0000000..8af5d8d
--- /dev/null
+++ b/src/assets/icons/employeePage/bag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/employeePage/card.svg b/src/assets/icons/employeePage/card.svg
new file mode 100644
index 0000000..43a2d44
--- /dev/null
+++ b/src/assets/icons/employeePage/card.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/employeePage/pen.svg b/src/assets/icons/employeePage/pen.svg
new file mode 100644
index 0000000..05eb8ee
--- /dev/null
+++ b/src/assets/icons/employeePage/pen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/recruitmentInfo/area.svg b/src/assets/icons/recruitmentInfo/area.svg
new file mode 100644
index 0000000..71ba9d1
--- /dev/null
+++ b/src/assets/icons/recruitmentInfo/area.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/icons/recruitmentInfo/salary.svg b/src/assets/icons/recruitmentInfo/salary.svg
new file mode 100644
index 0000000..63504a7
--- /dev/null
+++ b/src/assets/icons/recruitmentInfo/salary.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/features/employer/CompanyInfo/coupang.png b/src/assets/images/coupang.png
similarity index 100%
rename from src/features/employer/CompanyInfo/coupang.png
rename to src/assets/images/coupang.png
diff --git a/src/assets/images/profile-image.svg b/src/assets/images/profile-image.svg
new file mode 100644
index 0000000..3e57953
--- /dev/null
+++ b/src/assets/images/profile-image.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/common/AsyncBoundary/index.tsx b/src/components/common/AsyncBoundary/index.tsx
new file mode 100644
index 0000000..116c847
--- /dev/null
+++ b/src/components/common/AsyncBoundary/index.tsx
@@ -0,0 +1,30 @@
+import type { ComponentProps, Ref } from 'react';
+import { forwardRef, Suspense, useImperativeHandle, useRef, ReactNode } from 'react';
+import type { ErrorBoundaryProps } from 'react-error-boundary';
+import { ErrorBoundary } from 'react-error-boundary';
+
+type Props = {
+ children: ReactNode;
+ rejectedFallback?: ErrorBoundaryProps['fallback'];
+ pendingFallback?: ComponentProps['fallback'];
+};
+
+interface ResetRef {
+ reset?(): void;
+}
+
+const AsyncBoundary = forwardRef(({ pendingFallback, rejectedFallback, children }: Props, resetRef: Ref) => {
+ const ref = useRef(null);
+
+ useImperativeHandle(resetRef, () => ({
+ reset: () => ref.current?.resetErrorBoundary(),
+ }));
+
+ return (
+ >}>
+ >}>{children}
+
+ );
+});
+
+export default AsyncBoundary;
diff --git a/src/components/common/Button/index.stories.tsx b/src/components/common/Button/index.stories.tsx
index e908acd..d666695 100644
--- a/src/components/common/Button/index.stories.tsx
+++ b/src/components/common/Button/index.stories.tsx
@@ -7,7 +7,7 @@ const meta = {
tags: ['autodocs'],
args: {
children: '버튼',
- theme: 'default',
+ design: 'default',
},
render: (props) => ,
} satisfies Meta;
@@ -16,5 +16,6 @@ export default meta;
type Story = StoryObj;
export const Default: Story = {};
-export const Outlined: Story = { args: { theme: 'outlined' } };
-export const TextButton: Story = { args: { theme: 'textbutton' } };
+export const Outlined: Story = { args: { design: 'outlined' } };
+export const TextButton: Story = { args: { design: 'textbutton' } };
+export const DeactivateButton: Story = { args: { design: 'deactivate' } };
diff --git a/src/components/common/Button/index.tsx b/src/components/common/Button/index.tsx
index 1d59aa9..4e08944 100644
--- a/src/components/common/Button/index.tsx
+++ b/src/components/common/Button/index.tsx
@@ -1,14 +1,14 @@
import styled from '@emotion/styled';
-interface Props extends React.ButtonHTMLAttributes {
- theme?: 'default' | 'outlined' | 'textbutton';
+export interface Props extends React.ButtonHTMLAttributes {
+ design?: 'default' | 'outlined' | 'textbutton' | 'deactivate';
}
export default function Button({ children, ...props }: Props) {
return {children};
}
-const Wrapper = styled.button(
+const Wrapper = styled.button(
{
padding: '10px 40px',
fontSize: '16px',
@@ -20,8 +20,8 @@ const Wrapper = styled.button(
cursor: 'pointer',
transition: 'background-color 200ms',
},
- ({ theme = 'default' }) => {
- if (theme === 'outlined') {
+ ({ design = 'default' }) => {
+ if (design === 'outlined') {
return {
backgroundColor: '#fff',
color: '#0A65CC',
@@ -34,13 +34,20 @@ const Wrapper = styled.button(
};
}
- if (theme === 'textbutton') {
+ if (design === 'textbutton') {
return {
color: '#0A65CC',
backgroundColor: '#fff',
};
}
+ if (design === 'deactivate') {
+ return {
+ color: '#9A9A9A',
+ backgroundColor: '#D9D9D9',
+ };
+ }
+
return {
color: '#0A65CC',
backgroundColor: '#E7F0FA',
diff --git a/src/components/common/Icon/Arrow.ts b/src/components/common/Icon/Arrow.ts
index e7a5bcd..147b9d5 100644
--- a/src/components/common/Icon/Arrow.ts
+++ b/src/components/common/Icon/Arrow.ts
@@ -1,9 +1,13 @@
import RightWhite from '@assets/icons/arrow/right-white.svg?react';
import RightBlue from '@assets/icons/arrow/right-blue.svg?react';
+import DownBlue from '@assets/icons/arrow/down-blue.svg?react';
+import BigRightBlue from '@assets/icons/arrow/big-right-blue.svg?react';
const Arrow = {
RightWhite,
RightBlue,
+ DownBlue,
+ BigRightBlue,
};
export default Arrow;
diff --git a/src/components/common/Icon/EmployeePage.ts b/src/components/common/Icon/EmployeePage.ts
new file mode 100644
index 0000000..b5a3924
--- /dev/null
+++ b/src/components/common/Icon/EmployeePage.ts
@@ -0,0 +1,11 @@
+import Bag from '@assets/icons/employeePage/bag.svg?react';
+import Pen from '@assets/icons/employeePage/pen.svg?react';
+import Card from '@assets/icons/employeePage/card.svg?react';
+
+const EmployeePage = {
+ Bag,
+ Pen,
+ Card,
+};
+
+export default EmployeePage;
diff --git a/src/components/common/Icon/index.ts b/src/components/common/Icon/index.ts
index 7eef1b2..cd08c32 100644
--- a/src/components/common/Icon/index.ts
+++ b/src/components/common/Icon/index.ts
@@ -1,4 +1,5 @@
import Arrow from './Arrow';
+import EmployeePage from './EmployeePage';
import Role from './Role';
import Social from './Social';
@@ -6,6 +7,7 @@ const Icon = {
Role,
Social,
Arrow,
+ EmployeePage,
};
export default Icon;
diff --git a/src/components/common/InnerContainer/index.tsx b/src/components/common/InnerContainer/index.tsx
index 5240543..5a5253f 100644
--- a/src/components/common/InnerContainer/index.tsx
+++ b/src/components/common/InnerContainer/index.tsx
@@ -2,28 +2,27 @@ import { breakpoints } from '@assets/styles/global/breakpoints';
import { responsiveStyle } from '@utils/responsive';
import { HTMLAttributes, ReactNode } from 'react';
+const containerStyle = responsiveStyle({
+ default: {
+ maxWidth: '1300px',
+ margin: '0 auto',
+ },
+ tablet: {
+ maxWidth: breakpoints.tablet,
+ padding: '0 12px',
+ },
+ mobile: {
+ maxWidth: breakpoints.mobile,
+ },
+});
+
type Props = {
children: ReactNode;
} & HTMLAttributes;
export default function InnerContainer({ children, ...rest }: Props) {
return (
-
+
{children}
);
diff --git a/src/components/common/List/index.tsx b/src/components/common/List/index.tsx
index 78f0ec8..ba0efc0 100644
--- a/src/components/common/List/index.tsx
+++ b/src/components/common/List/index.tsx
@@ -6,5 +6,5 @@ interface Props
{
}
export default function List({ items, renderItem }: Props) {
- return <>{items.map((item) => renderItem(item))}>;
+ return <>{items.map(renderItem)}>;
}
diff --git a/src/components/common/Modal/Modals.tsx b/src/components/common/Modal/Modals.tsx
new file mode 100644
index 0000000..ff21b81
--- /dev/null
+++ b/src/components/common/Modal/Modals.tsx
@@ -0,0 +1,30 @@
+import { useContext } from 'react';
+import { ModalsDispatchContext, ModalsStateContext } from './index.context';
+import loadable from '@loadable/component';
+
+export interface ModalProps {
+ [key: string]: unknown;
+}
+
+export const modals = {
+ roleModal: loadable(() => import('@features/auth/SignUp/components/common/RoleModal')),
+};
+
+export default function Modals() {
+ const openedModals = useContext(ModalsStateContext);
+ const { close } = useContext(ModalsDispatchContext);
+
+ return openedModals.map((modal, index) => {
+ const { Component, props } = modal;
+ const { onSubmit, ...restProps } = props;
+
+ const handleClose = () => close(Component);
+
+ const handleSubmit = async () => {
+ if (typeof onSubmit === 'function') await onSubmit();
+ handleClose();
+ };
+
+ return ;
+ });
+}
diff --git a/src/components/common/Modal/hooks/useModals.ts b/src/components/common/Modal/hooks/useModals.ts
new file mode 100644
index 0000000..a56575d
--- /dev/null
+++ b/src/components/common/Modal/hooks/useModals.ts
@@ -0,0 +1,19 @@
+import { useContext } from 'react';
+import { ModalsDispatchContext } from '../index.context';
+
+export default function useModals() {
+ const { open, close } = useContext(ModalsDispatchContext);
+
+ const openModal = (Component: React.ComponentType, props: any) => {
+ open(Component, props);
+ };
+
+ const closeModal = (Component: React.ComponentType) => {
+ close(Component);
+ };
+
+ return {
+ openModal,
+ closeModal,
+ };
+}
diff --git a/src/components/common/Modal/index.context.tsx b/src/components/common/Modal/index.context.tsx
new file mode 100644
index 0000000..572c9d0
--- /dev/null
+++ b/src/components/common/Modal/index.context.tsx
@@ -0,0 +1,18 @@
+import { createContext } from 'react';
+
+interface ModalsDispatchContextProps {
+ open: (Component: React.ComponentType, props: any) => void;
+ close: (Component: React.ComponentType) => void;
+}
+
+export const ModalsDispatchContext = createContext({
+ open: () => {},
+ close: () => {},
+});
+
+interface ModalState {
+ Component: React.ComponentType;
+ props: any;
+}
+
+export const ModalsStateContext = createContext([]);
diff --git a/src/components/common/Modal/index.tsx b/src/components/common/Modal/index.tsx
index ae0e2df..6e60a74 100644
--- a/src/components/common/Modal/index.tsx
+++ b/src/components/common/Modal/index.tsx
@@ -4,14 +4,13 @@ import { ReactNode } from 'react';
export type Props = {
textChildren: ReactNode;
buttonChildren: ReactNode;
- borderRadius?: string;
onClose: () => void;
} & React.HTMLAttributes;
-const Modal = ({ textChildren, buttonChildren, borderRadius = '12px', onClose, ...props }: Props) => {
+const Modal = ({ textChildren, buttonChildren, onClose, ...props }: Props) => {
return (
- e.stopPropagation()}>
+ e.stopPropagation()}>
{textChildren}
{buttonChildren}
diff --git a/src/components/common/Select/hooks/useGlobalSelect.ts b/src/components/common/Select/hooks/useGlobalSelect.ts
new file mode 100644
index 0000000..41e22fc
--- /dev/null
+++ b/src/components/common/Select/hooks/useGlobalSelect.ts
@@ -0,0 +1,25 @@
+import { useSelectStore } from '@/store/selectStore';
+import { SelectOptionType } from '@/types/select';
+import { useEffect } from 'react';
+
+const useGlobalSelect = (initialOption: SelectOptionType) => {
+ const { selectedOption, setSelectedOption, initializeOption } = useSelectStore();
+
+ useEffect(() => {
+ if (!selectedOption) {
+ initializeOption(initialOption);
+ }
+ }, [selectedOption, initializeOption, initialOption]);
+
+ const handleSelect = (option: SelectOptionType) => {
+ setSelectedOption(option);
+ option.action();
+ };
+
+ return {
+ selectedOption: selectedOption || initialOption,
+ handleSelect,
+ };
+};
+
+export default useGlobalSelect;
diff --git a/src/components/common/Select/hooks/useSelect.ts b/src/components/common/Select/hooks/useSelect.ts
new file mode 100644
index 0000000..be47d81
--- /dev/null
+++ b/src/components/common/Select/hooks/useSelect.ts
@@ -0,0 +1,20 @@
+import { SelectOptionType } from '@/types/select';
+import { useState } from 'react';
+
+const useSelect = (defaultOption: SelectOptionType) => {
+ const [selectedOption, setSelectedOption] = useState(defaultOption);
+
+ const handleSelect = (option: SelectOptionType) => {
+ if (option.value !== selectedOption.value) {
+ setSelectedOption(option);
+ option.action();
+ }
+ };
+
+ return {
+ selectedOption,
+ handleSelect,
+ };
+};
+
+export default useSelect;
diff --git a/src/components/common/Select/index.context.tsx b/src/components/common/Select/index.context.tsx
new file mode 100644
index 0000000..8b1207f
--- /dev/null
+++ b/src/components/common/Select/index.context.tsx
@@ -0,0 +1,35 @@
+import useToggle from '@/hooks/useToggle';
+import { createContext, PropsWithChildren, useContext, useState } from 'react';
+
+interface SelectContextProps {
+ isOpen: boolean;
+ toggle: () => void;
+ selectedValue?: string;
+ onItemSelect: (value: string) => void;
+}
+
+const SelectContext = createContext(undefined);
+
+export const useSelectContext = () => {
+ const context = useContext(SelectContext);
+ if (!context) {
+ throw new Error('useSelectContext must be used within a useSelectContext');
+ }
+ return context;
+};
+
+export const SelectProvider = ({ children }: PropsWithChildren) => {
+ const [isOpen, toggle] = useToggle();
+ const [selectedValue, setSelectedValue] = useState(undefined);
+
+ const handleItemSelect = (value: string) => {
+ setSelectedValue(value);
+ toggle();
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/common/Select/index.stories.tsx b/src/components/common/Select/index.stories.tsx
new file mode 100644
index 0000000..0775338
--- /dev/null
+++ b/src/components/common/Select/index.stories.tsx
@@ -0,0 +1,109 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import Select from '.';
+import { SelectOptionType } from '@/types/select';
+import useGlobalSelect from './hooks/useGlobalSelect';
+import { Icon } from '@/components/common';
+
+const options: SelectOptionType[] = [
+ { value: 'korean', text: '한국어', action: () => console.log('한국어 선택') },
+ { value: 'english', text: '영어', action: () => console.log('영어 선택') },
+ { value: 'vietnamese', text: '베트남어', action: () => console.log('베트남어 선택') },
+];
+
+const meta: Meta = {
+ title: 'common/Select',
+ component: Select,
+ tags: ['autodocs'],
+ argTypes: {
+ icon: {
+ control: 'boolean',
+ description: '아이콘을 표시할지 여부를 결정',
+ },
+ options: {
+ control: 'object',
+ description: '선택할 수 있는 옵션 목록',
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: (args) => {
+ const { selectedOption, handleSelect } = useGlobalSelect(options[0]);
+
+ return (
+
+ }>
+ {selectedOption.text}
+
+
+ {options.map((option) => (
+ handleSelect(option)}>
+ {option.text}
+
+ ))}
+
+
+ );
+ },
+ args: {
+ icon: true,
+ options,
+ },
+};
+
+export const NoIcon: Story = {
+ render: (args) => {
+ const { selectedOption, handleSelect } = useGlobalSelect(options[0]);
+
+ return (
+
+ {selectedOption.text}
+
+ {options.map((option) => (
+ handleSelect(option)}>
+ {option.text}
+
+ ))}
+
+
+ );
+ },
+ args: {
+ icon: false,
+ options,
+ },
+};
+
+export const WithMultipleOptions: Story = {
+ render: (args) => {
+ const { selectedOption, handleSelect } = useGlobalSelect(options[0]);
+
+ return (
+
+ }>
+ {selectedOption.text}
+
+
+ {options.map((option) => (
+ handleSelect(option)}>
+ {option.text}
+
+ ))}
+
+
+ );
+ },
+ args: {
+ icon: true,
+ options: [
+ { value: 'korean', text: '한국어', action: () => console.log('한국어 선택') },
+ { value: 'english', text: '영어', action: () => console.log('영어 선택') },
+ { value: 'vietnamese', text: '베트남어', action: () => console.log('베트남어 선택') },
+ { value: 'japanese', text: '일본어', action: () => console.log('일본어 선택') },
+ ],
+ },
+};
diff --git a/src/components/common/Select/index.tsx b/src/components/common/Select/index.tsx
new file mode 100644
index 0000000..6c9f845
--- /dev/null
+++ b/src/components/common/Select/index.tsx
@@ -0,0 +1,13 @@
+import { SelectRoot as Root } from './sub-components/Root';
+import { SelectTrigger as Trigger } from './sub-components/Trigger';
+import { SelectContent as Content } from './sub-components/Content';
+import { SelectOption as Option } from './sub-components/Option';
+
+const Select = Object.assign({
+ Root,
+ Trigger,
+ Content,
+ Option,
+});
+
+export default Select;
diff --git a/src/components/common/Select/sub-components/Content.tsx b/src/components/common/Select/sub-components/Content.tsx
new file mode 100644
index 0000000..e0cfbed
--- /dev/null
+++ b/src/components/common/Select/sub-components/Content.tsx
@@ -0,0 +1,19 @@
+import styled from '@emotion/styled';
+import { useSelectContext } from '../index.context';
+
+export const SelectContent = ({ children }: { children: React.ReactNode }) => {
+ const { isOpen } = useSelectContext();
+
+ return {children};
+};
+
+export const Content = styled.div<{ isOpen: boolean }>`
+ position: absolute;
+ top: 100%;
+ left: 0;
+ background: ${({ theme }) => theme.palettes.white};
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ display: ${({ isOpen }) => (isOpen ? 'block' : 'none')};
+ min-width: 160px;
+ z-index: 1000;
+`;
diff --git a/src/components/common/Select/sub-components/Option.tsx b/src/components/common/Select/sub-components/Option.tsx
new file mode 100644
index 0000000..aef7a87
--- /dev/null
+++ b/src/components/common/Select/sub-components/Option.tsx
@@ -0,0 +1,36 @@
+import { PropsWithChildren } from 'react';
+import { useSelectContext } from '../index.context';
+import styled from '@emotion/styled';
+
+type Props = {
+ value: string;
+ onClick?: () => void;
+} & PropsWithChildren;
+
+export const SelectOption = ({ children, value, onClick, ...rest }: Props) => {
+ const { selectedValue, onItemSelect } = useSelectContext();
+
+ const handleOptionClick = () => {
+ if (onClick) onClick();
+ onItemSelect(value);
+ };
+
+ return (
+
+ );
+};
+
+export const Option = styled.div<{ isSelected: boolean }>`
+ padding: 8px 12px;
+ cursor: pointer;
+ color: ${({ theme, isSelected }) => (isSelected ? theme.palettes.blue : theme.palettes.black)};
+ font-weight: ${({ isSelected }) => (isSelected ? 700 : 400)};
+
+ &:hover {
+ background-color: #f1f1f1;
+ color: ${({ theme }) => theme.palettes.blue};
+ font-weight: 700;
+ }
+`;
diff --git a/src/components/common/Select/sub-components/Root.tsx b/src/components/common/Select/sub-components/Root.tsx
new file mode 100644
index 0000000..e1c6736
--- /dev/null
+++ b/src/components/common/Select/sub-components/Root.tsx
@@ -0,0 +1,15 @@
+import styled from '@emotion/styled';
+import { SelectProvider } from '../index.context';
+
+export const SelectRoot = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const Root = styled.div`
+ position: relative;
+ display: inline-block;
+`;
diff --git a/src/components/common/Select/sub-components/Trigger.tsx b/src/components/common/Select/sub-components/Trigger.tsx
new file mode 100644
index 0000000..e67dcb2
--- /dev/null
+++ b/src/components/common/Select/sub-components/Trigger.tsx
@@ -0,0 +1,26 @@
+import { Flex } from '@components/common';
+import { useSelectContext } from '../index.context';
+import { PropsWithChildren, ReactNode } from 'react';
+import styled from '@emotion/styled';
+
+type Props = {
+ icon?: ReactNode;
+} & PropsWithChildren;
+
+export const SelectTrigger = ({ icon, children, ...rest }: Props) => {
+ const { toggle } = useSelectContext();
+
+ return (
+
+ );
+};
+
+const Icon = styled.div`
+ display: flex;
+ align-items: center;
+`;
diff --git a/src/components/common/Spinner/index.stories.tsx b/src/components/common/Spinner/index.stories.tsx
new file mode 100644
index 0000000..6cde837
--- /dev/null
+++ b/src/components/common/Spinner/index.stories.tsx
@@ -0,0 +1,14 @@
+import { Meta, StoryObj } from '@storybook/react';
+import Spinner from '.';
+
+const meta: Meta = {
+ title: 'common/Spinner',
+ component: Spinner,
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/components/common/Spinner/index.tsx b/src/components/common/Spinner/index.tsx
new file mode 100644
index 0000000..909c4e7
--- /dev/null
+++ b/src/components/common/Spinner/index.tsx
@@ -0,0 +1,27 @@
+import { css } from '@emotion/react';
+
+const spinnerStyle = css`
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ border: 4px solid rgba(0, 0, 0, 0.1);
+ border-left-color: #09f;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ animation: spin 1s linear infinite;
+
+ @keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+`;
+
+export default function Spinner() {
+ return ;
+}
diff --git a/src/components/common/Table/index.stories.tsx b/src/components/common/Table/index.stories.tsx
new file mode 100644
index 0000000..19a068e
--- /dev/null
+++ b/src/components/common/Table/index.stories.tsx
@@ -0,0 +1,38 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Table, Td, Th } from '.';
+
+const meta: Meta = {
+ title: 'common/Table',
+ component: Table,
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: (args) => (
+
+
+
+ heading 1 |
+ heading 2 |
+ heading 3 |
+
+
+
+
+ data 1 |
+ data 2 |
+ data 3 |
+
+
+ data 4 |
+ data 5 |
+ data 6 |
+
+
+
+ ),
+};
diff --git a/src/components/common/Table/index.style.ts b/src/components/common/Table/index.style.ts
new file mode 100644
index 0000000..163f117
--- /dev/null
+++ b/src/components/common/Table/index.style.ts
@@ -0,0 +1,33 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+
+export const TableStyle = css`
+ ${responsiveStyle({
+ default: {
+ width: '100%',
+ fontSize: '16px',
+ borderCollapse: 'collapse',
+ wordWrap: 'break-word',
+ },
+ tablet: {
+ fontSize: '15px',
+ },
+ })}
+`;
+
+export const ThStyle = css`
+ padding: 10px 30px;
+ background-color: ${palettes.backgroundGray};
+ border: none;
+ border-top: 1px solid ${palettes.black};
+ border-bottom: 1px solid ${palettes.black};
+ text-align: left;
+`;
+
+export const TdStyle = css`
+ padding: 20px 30px;
+ border: none;
+ border-bottom: 1px solid ${palettes.borderGray};
+ text-align: left;
+`;
diff --git a/src/components/common/Table/index.tsx b/src/components/common/Table/index.tsx
new file mode 100644
index 0000000..7b81a38
--- /dev/null
+++ b/src/components/common/Table/index.tsx
@@ -0,0 +1,38 @@
+import { HTMLAttributes, ReactNode } from 'react';
+import { TableStyle, TdStyle, ThStyle } from './index.style';
+
+type TableProps = {
+ children: ReactNode;
+} & HTMLAttributes;
+
+type ThProps = {
+ children?: ReactNode;
+} & HTMLAttributes;
+
+type TdProps = {
+ children: ReactNode;
+} & HTMLAttributes;
+
+export function Table({ children, ...rest }: TableProps) {
+ return (
+
+ );
+}
+
+export function Th({ children, ...rest }: ThProps) {
+ return (
+
+ {children}
+ |
+ );
+}
+
+export function Td({ children, ...rest }: TdProps) {
+ return (
+
+ {children}
+ |
+ );
+}
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index cfae363..3d45851 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -8,3 +8,7 @@ export { default as Typo } from './Typo';
export { default as Icon } from './Icon';
export { default as List } from './List';
export { default as Image } from './Image';
+export { default as Select } from './Select';
+export { default as AsyncBoundary } from './AsyncBoundary';
+export { default as Spinner } from './Spinner';
+export { Table, Th, Td } from './Table';
diff --git a/src/components/providers/Modals.provider.tsx b/src/components/providers/Modals.provider.tsx
new file mode 100644
index 0000000..7beca6b
--- /dev/null
+++ b/src/components/providers/Modals.provider.tsx
@@ -0,0 +1,29 @@
+import { ModalsDispatchContext, ModalsStateContext } from '../common/Modal/index.context';
+import { PropsWithChildren, useState, useMemo } from 'react';
+
+interface ModalState {
+ Component: React.ComponentType;
+ props: any;
+}
+
+const ModalsProvider = ({ children }: PropsWithChildren) => {
+ const [openedModals, setOpenedModals] = useState([]);
+
+ const open = (Component: React.ComponentType, props: any) => {
+ setOpenedModals((modals) => [...modals, { Component, props }]);
+ };
+
+ const close = (Component: React.ComponentType) => {
+ setOpenedModals((modals) => modals.filter((modal) => modal.Component !== Component));
+ };
+
+ const dispatch = useMemo(() => ({ open, close }), []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ModalsProvider;
diff --git a/src/components/providers/index.provider.tsx b/src/components/providers/index.provider.tsx
index 4b12809..36597b8 100644
--- a/src/components/providers/index.provider.tsx
+++ b/src/components/providers/index.provider.tsx
@@ -1,6 +1,19 @@
import { ReactNode } from 'react';
import GlobalStylesProvider from './GlobalStylesProvider/index.provider';
+import ModalsProvider from './Modals.provider';
+import Modals from '../common/Modal/Modals';
+import { QueryClientProvider } from '@tanstack/react-query';
+import { queryClient } from '@/apis/instance';
export default function AppProviders({ children }: { children: ReactNode }) {
- return {children};
+ return (
+
+
+
+ {children}
+
+
+
+
+ );
}
diff --git a/src/features/applicants/ApplicantList/ApplicantsTable/index.styles.ts b/src/features/applicants/ApplicantList/ApplicantsTable/index.styles.ts
new file mode 100644
index 0000000..41faa55
--- /dev/null
+++ b/src/features/applicants/ApplicantList/ApplicantsTable/index.styles.ts
@@ -0,0 +1,37 @@
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+
+export const buttonsCellStyle = css`
+ ${responsiveStyle({
+ default: {
+ width: '600px',
+ },
+ tablet: {
+ width: '280px',
+ },
+ mobile: {
+ width: '200px',
+ },
+ })}
+`;
+
+export const buttonGroupStyle = css`
+ ${responsiveStyle({
+ tablet: {
+ flexDirection: 'column',
+ alignItems: 'stretch',
+ gap: '10px',
+ },
+ })}
+`;
+
+export const buttonStyle = css`
+ ${responsiveStyle({
+ default: {
+ whiteSpace: 'nowrap',
+ },
+ mobile: {
+ fontSize: '15px',
+ },
+ })}
+`;
diff --git a/src/features/applicants/ApplicantList/ApplicantsTable/index.tsx b/src/features/applicants/ApplicantList/ApplicantsTable/index.tsx
new file mode 100644
index 0000000..945bedc
--- /dev/null
+++ b/src/features/applicants/ApplicantList/ApplicantsTable/index.tsx
@@ -0,0 +1,58 @@
+import { Button, Flex, List, Table, Td, Th } from '@/components/common';
+import { useState } from 'react';
+import ContractModal from '../ContractModal';
+import { ApplicantData } from '@/types';
+import { buttonGroupStyle, buttonsCellStyle, buttonStyle } from './index.styles';
+
+type Props = {
+ applicantList: ApplicantData[];
+};
+
+export default function ApplicantsTable({ applicantList }: Props) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const handleOpenModal = () => {
+ setIsModalOpen(true);
+ };
+
+ const handleCloseModal = () => {
+ setIsModalOpen(false);
+ };
+
+ return (
+ <>
+
+
+
+ 이름 |
+ 국적 |
+ 한국어 실력 |
+ |
+
+
+
+ (
+
+ {applicant.name} |
+ {applicant.applicantNation} |
+ {applicant.korean} |
+
+
+
+
+
+
+ |
+
+ )}
+ />
+
+
+
+ >
+ );
+}
diff --git a/src/features/applicants/ApplicantList/ContractModal/ModalText/index.styles.ts b/src/features/applicants/ApplicantList/ContractModal/ModalText/index.styles.ts
new file mode 100644
index 0000000..5966ad7
--- /dev/null
+++ b/src/features/applicants/ApplicantList/ContractModal/ModalText/index.styles.ts
@@ -0,0 +1,13 @@
+export const titleStyle = {
+ fontWeight: 'bold',
+ marginBottom: '20px',
+};
+
+export const paragraphStyle = {
+ marginBottom: '20px',
+};
+
+export const headingStyle = {
+ ...titleStyle,
+ marginTop: '20px',
+};
diff --git a/src/features/applicants/ApplicantList/ContractModal/ModalText/index.tsx b/src/features/applicants/ApplicantList/ContractModal/ModalText/index.tsx
new file mode 100644
index 0000000..6b8dcce
--- /dev/null
+++ b/src/features/applicants/ApplicantList/ContractModal/ModalText/index.tsx
@@ -0,0 +1,44 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { ForeignerData } from '@/types';
+import { Typo } from '@/components/common';
+import { headingStyle, paragraphStyle, titleStyle } from './index.styles';
+
+type Props = Pick;
+
+export default function ModalText({ foreignerIdNumber, visaGenerateDate }: Props) {
+ return (
+ <>
+
+ ✅ 고용주님께 드리는 주의사항
+
+
+ 불법 체류자를 고용할 시 최대 200만원의 범칙금이 부과될 수 있습니다.
+
+
+
+ 지원자 정보
+
+
+ 외국인 등록 번호 : {foreignerIdNumber}
+
+ 비자 발급 일자 : {visaGenerateDate}
+
+
이 지원자의 정보를 활용하여
+
+ 하이코리아
+
+ 에서 지원자에 대한
+
+ 불법 체류 여부를 검증할 수 있습니다.
+
+
+ 안전한 고용을 위해 확인 후 진행하는 것을 권장합니다.
+
+ >
+ );
+}
diff --git a/src/features/applicants/ApplicantList/ContractModal/index.mock.ts b/src/features/applicants/ApplicantList/ContractModal/index.mock.ts
new file mode 100644
index 0000000..8093bf3
--- /dev/null
+++ b/src/features/applicants/ApplicantList/ContractModal/index.mock.ts
@@ -0,0 +1,5 @@
+export const foreigner = {
+ foreignerIdNumber: '123456-1234567',
+ visaGenerateDate: '2000-00-00',
+ visaExpiryDate: '2000-00-00',
+};
diff --git a/src/features/applicants/ApplicantList/ContractModal/index.styles.ts b/src/features/applicants/ApplicantList/ContractModal/index.styles.ts
new file mode 100644
index 0000000..42599dd
--- /dev/null
+++ b/src/features/applicants/ApplicantList/ContractModal/index.styles.ts
@@ -0,0 +1,15 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { css } from '@emotion/react';
+
+export const modalStyle = {
+ padding: '15px',
+};
+
+export const customButtonStyle = css`
+ background-color: ${palettes.blue};
+ color: ${palettes.white};
+`;
+
+export const buttonTextStyle = {
+ color: `${palettes.white}`,
+};
diff --git a/src/features/applicants/ApplicantList/ContractModal/index.tsx b/src/features/applicants/ApplicantList/ContractModal/index.tsx
new file mode 100644
index 0000000..0afbe7b
--- /dev/null
+++ b/src/features/applicants/ApplicantList/ContractModal/index.tsx
@@ -0,0 +1,39 @@
+import { Button, Flex, Icon, Modal, Typo } from '@/components/common';
+import ModalText from './ModalText';
+import { foreigner } from './index.mock';
+import { buttonTextStyle, customButtonStyle, modalStyle } from './index.styles';
+
+interface ContractModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export default function ContractModal({ isOpen, onClose }: ContractModalProps) {
+ return (
+ <>
+ {isOpen && (
+
+ }
+ buttonChildren={
+
+
+
+
+ }
+ /* onClose 부분 추후 수정 예정 */
+ onClose={onClose}
+ style={modalStyle}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/features/applicants/ApplicantList/index.tsx b/src/features/applicants/ApplicantList/index.tsx
new file mode 100644
index 0000000..a355b49
--- /dev/null
+++ b/src/features/applicants/ApplicantList/index.tsx
@@ -0,0 +1,23 @@
+import { Flex, Typo } from '@/components/common';
+import ApplicantsTable from './ApplicantsTable';
+import { ApplicantData } from '@/types';
+
+type Props = {
+ applicantList: ApplicantData[];
+};
+
+export default function ApplicantList({ applicantList }: Props) {
+ return (
+
+
+
+ 지원자 목록
+
+
+ 총 {applicantList.length}건
+
+
+
+
+ );
+}
diff --git a/src/features/auth/RoleModal.tsx b/src/features/auth/RoleModal.tsx
deleted file mode 100644
index 52e9cac..0000000
--- a/src/features/auth/RoleModal.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Modal, Typo, Flex, Button } from '@components/common';
-import { ReactNode } from 'react';
-
-type Props = {
- content: ReactNode;
- onClose: () => void;
-};
-
-export default function RoleModal({ content, onClose }: Props) {
- return (
-
-
- 정보를 입력해주세요.
-
- {content}
-
-
- * 추후 마이페이지에서 수정 할 수 있습니다.
-
-
-
- }
- buttonChildren={
-
-
-
-
- }
- onClose={onClose}
- />
- );
-}
diff --git a/src/features/auth/RoleSelection.tsx b/src/features/auth/RoleSelection.tsx
deleted file mode 100644
index 55a16ec..0000000
--- a/src/features/auth/RoleSelection.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Flex } from '@components/common';
-import RoleSelector from './components/RoleSelector';
-import { ReactNode } from 'react';
-
-type Props = {
- onRoleSelect: (modalContent: ReactNode) => void;
-};
-
-export default function RoleSelection({ onRoleSelect }: Props) {
- return (
-
-
-
-
- );
-}
diff --git a/src/features/auth/SignIn/components/SignInButton.tsx b/src/features/auth/SignIn/components/SignInButton.tsx
new file mode 100644
index 0000000..3e060ab
--- /dev/null
+++ b/src/features/auth/SignIn/components/SignInButton.tsx
@@ -0,0 +1,20 @@
+import { useGoogleOAuth } from '@/apis/auth/mutations/useGoogleOAuth';
+import { Flex, Typo, Button, Icon } from '@components/common';
+
+const FLEX_GAP_CONFIG = { x: '12px' };
+const BUTTON_STYLE = { fontWeight: '300' };
+
+export function SignInButton() {
+ const { redirectToGoogleLogin } = useGoogleOAuth();
+
+ return (
+
+ );
+}
diff --git a/src/features/auth/SignIn/components/SignInText.tsx b/src/features/auth/SignIn/components/SignInText.tsx
new file mode 100644
index 0000000..cdcfd42
--- /dev/null
+++ b/src/features/auth/SignIn/components/SignInText.tsx
@@ -0,0 +1,41 @@
+import { Flex, Typo } from '@components/common';
+import { responsiveStyle } from '@utils/responsive';
+
+const flexStyle = responsiveStyle({
+ default: {
+ marginBottom: '72px',
+ },
+ tablet: {
+ marginBottom: '56px',
+ },
+ mobile: {
+ marginBottom: '42px',
+ alignItems: 'center',
+ },
+});
+
+const headingTypoStyle = responsiveStyle({
+ default: {
+ marginBottom: '24px',
+ },
+ tablet: { fontSize: '32px' },
+ mobile: { fontSize: '28px' },
+});
+
+const paragraphTypoStyle = responsiveStyle({
+ tablet: { fontSize: '16px' },
+ mobile: { fontSize: '14px' },
+});
+
+export function SignInText() {
+ return (
+
+
+ 지금 바로 시작하세요. 🚀
+
+
+ 안정적이고 투명한 고용 관계의 시작, 지금 바로 경험해보세요!
+
+
+ );
+}
diff --git a/src/features/auth/SignUp/components/RoleSelection.tsx b/src/features/auth/SignUp/components/RoleSelection.tsx
new file mode 100644
index 0000000..d6dac7a
--- /dev/null
+++ b/src/features/auth/SignUp/components/RoleSelection.tsx
@@ -0,0 +1,13 @@
+import { Flex } from '@components/common';
+import RoleSelector from './common/RoleSelector';
+
+const FLEX_GAP_CONFIG = { x: '30px' };
+
+export default function RoleSelection() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/features/auth/SignUp/components/SignUpText.tsx b/src/features/auth/SignUp/components/SignUpText.tsx
new file mode 100644
index 0000000..9ae4c27
--- /dev/null
+++ b/src/features/auth/SignUp/components/SignUpText.tsx
@@ -0,0 +1,41 @@
+import { Flex, Typo } from '@/components/common';
+import { responsiveStyle } from '@/utils/responsive';
+
+const titleStyle = responsiveStyle({
+ default: {
+ marginBottom: '38px',
+ },
+ tablet: {
+ marginBottom: '28px',
+ },
+ mobile: {
+ marginBottom: '20px',
+ fontSize: '20px',
+ },
+});
+
+const descriptionTitle = responsiveStyle({
+ default: {
+ marginBottom: '38px',
+ },
+ tablet: {
+ marginBottom: '28px',
+ },
+ mobile: {
+ marginBottom: '20px',
+ fontSize: '16px',
+ },
+});
+
+export default function SignUpText() {
+ return (
+
+
+ 가입자 정보 선택
+
+
+ 대상에 해당하는 가입자 정보를 선택해주세요.
+
+
+ );
+}
diff --git a/src/features/auth/SignUp/components/common/RoleModal.tsx b/src/features/auth/SignUp/components/common/RoleModal.tsx
new file mode 100644
index 0000000..55ccaa0
--- /dev/null
+++ b/src/features/auth/SignUp/components/common/RoleModal.tsx
@@ -0,0 +1,42 @@
+import { Modal, Typo, Flex, Button } from '@components/common';
+import { ReactNode } from 'react';
+
+const DEFAULT_CSS = { marginBottom: '12px' };
+const FLEX_GAP_CONFIG = { x: '16px' };
+
+type Props = {
+ content: ReactNode;
+ onSubmit: () => void;
+ onClose: () => void;
+};
+
+export default function RoleModal({ content, onSubmit, onClose }: Props) {
+ return (
+
+
+ 정보를 입력해주세요.
+
+
+ {content}
+
+
+ * 추후 마이페이지에서 수정 할 수 있습니다.
+
+
+ }
+ buttonChildren={
+
+
+
+
+ }
+ onClose={onClose}
+ />
+ );
+}
diff --git a/src/features/auth/components/RoleSelector/index.config.tsx b/src/features/auth/SignUp/components/common/RoleSelector/index.config.tsx
similarity index 81%
rename from src/features/auth/components/RoleSelector/index.config.tsx
rename to src/features/auth/SignUp/components/common/RoleSelector/index.config.tsx
index d725a5a..463c935 100644
--- a/src/features/auth/components/RoleSelector/index.config.tsx
+++ b/src/features/auth/SignUp/components/common/RoleSelector/index.config.tsx
@@ -1,5 +1,6 @@
import { responsiveStyle } from '@utils/responsive';
import { Icon } from '@components/common';
+import ROUTE_PATH from '@/routes/path';
export const roleConfig = {
employer: {
@@ -20,8 +21,9 @@ export const roleConfig = {
이력서 정보를 등록하러 가실까요?
>
),
+ toNavigate: ROUTE_PATH.HOME, // 이력서 등록 페이지로 변경
},
- worker: {
+ employee: {
icon: (
),
+ toNavigate: ROUTE_PATH.HOME, // 회사 등록 페이지로 변경
},
};
diff --git a/src/features/auth/SignUp/components/common/RoleSelector/index.tsx b/src/features/auth/SignUp/components/common/RoleSelector/index.tsx
new file mode 100644
index 0000000..e13b54f
--- /dev/null
+++ b/src/features/auth/SignUp/components/common/RoleSelector/index.tsx
@@ -0,0 +1,60 @@
+import { Card, Flex, Typo } from '@components/common';
+import { roleConfig } from './index.config';
+import { bounceAnimation } from '@assets/styles/animations';
+import { responsiveStyle } from '@utils/responsive';
+import useModals from '@components/common/Modal/hooks/useModals';
+import { modals } from '@/components/common/Modal/Modals';
+import { useRegister } from '@/apis/auth/mutations/useRegister';
+import { useNavigate } from 'react-router-dom';
+import ROUTE_PATH from '@/routes/path';
+
+const cardStyle = responsiveStyle({
+ default: { padding: '60px 120px', cursor: 'pointer' },
+ mobile: { padding: '16px 32px' },
+});
+
+const iconStyle = responsiveStyle({
+ default: { marginBottom: '24px' },
+ mobile: {
+ marginBottom: '10px',
+ },
+});
+
+type Props = {
+ role: 'employer' | 'employee';
+};
+
+export default function RoleSelector({ role }: Props) {
+ const navigate = useNavigate();
+ const { openModal } = useModals();
+ const { mutate } = useRegister();
+
+ const handleRegister = () => {
+ mutate(
+ { type: role },
+ {
+ onSuccess: () => {
+ handleOpenModal();
+ },
+ },
+ );
+ };
+
+ const handleOpenModal = () =>
+ openModal(modals.roleModal, {
+ content: roleConfig[role].modalContent,
+ onSubmit: () => navigate(roleConfig[role].toNavigate),
+ onClose: () => navigate(ROUTE_PATH.HOME),
+ });
+
+ return (
+
+
+ {roleConfig[role].icon}
+
+ {roleConfig[role].text}
+
+
+
+ );
+}
diff --git a/src/features/auth/components/RoleModal.tsx b/src/features/auth/components/RoleModal.tsx
deleted file mode 100644
index 52e9cac..0000000
--- a/src/features/auth/components/RoleModal.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Modal, Typo, Flex, Button } from '@components/common';
-import { ReactNode } from 'react';
-
-type Props = {
- content: ReactNode;
- onClose: () => void;
-};
-
-export default function RoleModal({ content, onClose }: Props) {
- return (
-
-
- 정보를 입력해주세요.
-
- {content}
-
-
- * 추후 마이페이지에서 수정 할 수 있습니다.
-
-
-
- }
- buttonChildren={
-
-
-
-
- }
- onClose={onClose}
- />
- );
-}
diff --git a/src/features/auth/components/RoleSelection.tsx b/src/features/auth/components/RoleSelection.tsx
deleted file mode 100644
index 4037afb..0000000
--- a/src/features/auth/components/RoleSelection.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Flex } from '@components/common';
-import RoleSelector from './RoleSelector';
-import { ReactNode } from 'react';
-
-type Props = {
- onRoleSelect: (modalContent: ReactNode) => void;
-};
-
-export default function RoleSelection({ onRoleSelect }: Props) {
- return (
-
-
-
-
- );
-}
diff --git a/src/features/auth/components/RoleSelector/index.tsx b/src/features/auth/components/RoleSelector/index.tsx
deleted file mode 100644
index 610d12f..0000000
--- a/src/features/auth/components/RoleSelector/index.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Card, Flex, Typo } from '@components/common';
-import { roleConfig } from './index.config';
-import { bounceAnimation } from '@assets/styles/animations';
-import { ReactNode } from 'react';
-import { responsiveStyle } from '@utils/responsive';
-
-type Props = {
- role: 'employer' | 'worker';
- onClick: (modalContent: ReactNode) => void;
-};
-
-export default function RoleSelector({ role, onClick }: Props) {
- return (
- onClick(roleConfig[role].modalContent)}
- >
-
-
- {roleConfig[role].icon}
-
-
- {roleConfig[role].text}
-
-
-
- );
-}
diff --git a/src/features/companies/CompanyInfo/index.styles.ts b/src/features/companies/CompanyInfo/index.styles.ts
new file mode 100644
index 0000000..f6f9fc0
--- /dev/null
+++ b/src/features/companies/CompanyInfo/index.styles.ts
@@ -0,0 +1,46 @@
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+
+export const infoWrapperStyle = css`
+ ${responsiveStyle({
+ default: {
+ maxWidth: '60%',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ gap: '15px',
+ },
+ tablet: {
+ alignItems: 'flex-start',
+ margin: '10px 0 20px 0',
+ width: '100%',
+ padding: '0 40px',
+ },
+ mobile: {
+ padding: '0',
+ },
+ })}
+`;
+
+export const companyNameStyle = {
+ fontWeight: 'bold',
+};
+
+export const infoStyle = css`
+ ${responsiveStyle({
+ default: {
+ alignItems: 'center',
+ gap: '30px',
+ },
+ tablet: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ width: '100%',
+ gap: '10px',
+ },
+ mobile: {
+ flexDirection: 'column',
+ alignItems: 'center',
+ gap: '10px',
+ },
+ })}
+`;
diff --git a/src/features/companies/CompanyInfo/index.tsx b/src/features/companies/CompanyInfo/index.tsx
new file mode 100644
index 0000000..ad90e69
--- /dev/null
+++ b/src/features/companies/CompanyInfo/index.tsx
@@ -0,0 +1,32 @@
+import { Flex, Typo } from '@/components/common';
+import { CompanyData } from '@/types';
+import IndustryIcon from '@assets/icons/companyInfo/industry.svg?react';
+import BrandIcon from '@assets/icons/companyInfo/brand.svg?react';
+import RevenueIcon from '@assets/icons/companyInfo/revenue.svg?react';
+import { companyNameStyle, infoStyle, infoWrapperStyle } from './index.styles';
+
+type Props = Pick;
+
+export default function CompanyInfo({ name, industryOccupation, brand, revenuePerYear }: Props) {
+ return (
+
+
+ {name}
+
+
+
+
+ {industryOccupation}
+
+
+
+ {brand}
+
+
+
+ {revenuePerYear} 원
+
+
+
+ );
+}
diff --git a/src/features/companies/CompanyList/CompaniesTable/index.styles.ts b/src/features/companies/CompanyList/CompaniesTable/index.styles.ts
new file mode 100644
index 0000000..49cddde
--- /dev/null
+++ b/src/features/companies/CompanyList/CompaniesTable/index.styles.ts
@@ -0,0 +1,26 @@
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+
+export const cellStyle = css`
+ ${responsiveStyle({
+ tablet: {
+ flexDirection: 'column',
+ },
+ })}
+`;
+
+export const imageSize = {
+ width: '277px',
+ height: '110px',
+};
+
+export const imageStyle = css`
+ height: 100%;
+
+ ${responsiveStyle({
+ tablet: {
+ margin: '0 auto',
+ width: '50%',
+ },
+ })}
+`;
diff --git a/src/features/companies/CompanyList/CompaniesTable/index.tsx b/src/features/companies/CompanyList/CompaniesTable/index.tsx
new file mode 100644
index 0000000..4e2433a
--- /dev/null
+++ b/src/features/companies/CompanyList/CompaniesTable/index.tsx
@@ -0,0 +1,45 @@
+import { Button, Flex, Icon, List, Image, Table, Th, Td } from '@/components/common';
+import { CompanyData } from '@/types';
+import CompanyInfo from '@/features/companies/CompanyInfo';
+import { cellStyle, imageSize, imageStyle } from './index.styles';
+
+type Props = {
+ companyList: CompanyData[];
+};
+
+export default function CompaniesTable({ companyList }: Props) {
+ return (
+
+
+
+ 회사 정보 |
+
+
+
+ (
+
+
+
+
+
+
+
+
+
+ |
+
+ )}
+ />
+
+
+ );
+}
diff --git a/src/features/companies/CompanyList/index.tsx b/src/features/companies/CompanyList/index.tsx
new file mode 100644
index 0000000..3975af0
--- /dev/null
+++ b/src/features/companies/CompanyList/index.tsx
@@ -0,0 +1,23 @@
+import { Flex, Typo } from '@/components/common';
+import CompaniesTable from './CompaniesTable';
+import { CompanyData } from '@/types';
+
+type Props = {
+ companyList: CompanyData[];
+};
+
+export default function CompanyList({ companyList }: Props) {
+ return (
+
+
+
+ 내 회사
+
+
+ 총 {companyList.length}곳
+
+
+
+
+ );
+}
diff --git a/src/features/employee/myPage/CardButton.tsx b/src/features/employee/myPage/CardButton.tsx
new file mode 100644
index 0000000..fc788ba
--- /dev/null
+++ b/src/features/employee/myPage/CardButton.tsx
@@ -0,0 +1,24 @@
+import { Button } from '@/components/common';
+import { ReactNode } from 'react';
+
+type Props = {
+ design?: 'default' | 'outlined' | 'textbutton' | 'deactivate';
+ children: ReactNode;
+};
+
+export default function CardButton({ design, children }: Props) {
+ return (
+
+ );
+}
diff --git a/src/features/employee/myPage/EmployeeProfile.tsx b/src/features/employee/myPage/EmployeeProfile.tsx
new file mode 100644
index 0000000..3805f0a
--- /dev/null
+++ b/src/features/employee/myPage/EmployeeProfile.tsx
@@ -0,0 +1,62 @@
+import { Card, Image, Typo, Button } from '@/components/common';
+import styled from '@emotion/styled';
+
+export default function EmployeeProfile({
+ profileImage = 'https://img.freepik.com/free-photo/user-profile-icon-front-side-with-white-background_187299-40010.jpg?t=st=1729752570~exp=1729756170~hmac=4313719023c412dd92883d97ce79956fadf541e11d8cc3a4ef05150f301f5e7f&w=740',
+ name = '홍길동',
+ description = '소개합니당',
+}) {
+ return (
+
+
+
+
+
+ {name}
+
+
+ {description}
+
+
+
+
+
+ );
+}
+
+const ProfileSection = styled.div`
+ display: flex;
+ justify-content: start;
+ align-items: center;
+`;
+
+const TextSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: start;
+ margin-left: 20px;
+`;
diff --git a/src/features/employee/myPage/MyRecruitCard.tsx b/src/features/employee/myPage/MyRecruitCard.tsx
new file mode 100644
index 0000000..6f094af
--- /dev/null
+++ b/src/features/employee/myPage/MyRecruitCard.tsx
@@ -0,0 +1,74 @@
+import { Card, Image, Typo, Button } from '@/components/common';
+import styled from '@emotion/styled';
+import { MyRecruitListProps, StateProps } from '@/types';
+
+type DesignProps = {
+ design: 'default' | 'outlined' | 'textbutton' | 'deactivate';
+ text: string;
+};
+
+function getStateStyle(state: StateProps): DesignProps {
+ switch (state) {
+ case '근로계약서 서명하기':
+ return { design: 'default', text: '근로계약서 서명하기' };
+ case '채용 마감':
+ return { design: 'deactivate', text: '채용 마감' };
+ case '지원서 검토중':
+ return { design: 'outlined', text: '지원서 검토중' };
+ case '채용 완료':
+ return { design: 'deactivate', text: '채용 완료' };
+ default:
+ return { design: 'deactivate', text: '알 수 없음' }; // 상태가 정의되지 않은 경우
+ }
+}
+
+type Props = {
+ myRecruit: MyRecruitListProps;
+};
+
+export default function MyRecruitCard({ myRecruit }: Props) {
+ const { image, title, area, state } = myRecruit;
+ const buttonStyle = getStateStyle(state);
+
+ return (
+
+
+
+
+ {title}
+
+ {area}
+
+
+
+ );
+}
+
+const TextSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ flex: 1;
+ padding: 0 10px;
+`;
diff --git a/src/features/employee/myPage/MyRecruitList.tsx b/src/features/employee/myPage/MyRecruitList.tsx
new file mode 100644
index 0000000..8fad8c5
--- /dev/null
+++ b/src/features/employee/myPage/MyRecruitList.tsx
@@ -0,0 +1,16 @@
+import { List } from '@/components/common';
+import { MyRecruitListProps } from '@/types';
+import MyRecruitCard from './MyRecruitCard';
+
+type Props = {
+ myRecruitList: MyRecruitListProps[];
+};
+
+export default function MyRecruitList({ myRecruitList }: Props) {
+ return (
+ }
+ />
+ );
+}
diff --git a/src/features/employer/CompanyInfo/index.tsx b/src/features/employer/CompanyInfo/index.tsx
deleted file mode 100644
index 1d92db4..0000000
--- a/src/features/employer/CompanyInfo/index.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { Button, Flex, Typo, Icon } from '@/components/common';
-import styled from '@emotion/styled';
-import IndustryIcon from '@assets/icons/companyInfo/industry.svg?react';
-import BrandIcon from '@assets/icons/companyInfo/brand.svg?react';
-import RevenueIcon from '@assets/icons/companyInfo/revenue.svg?react';
-import { responsiveStyle } from '@utils/responsive';
-
-interface CompanyInfoProps {
- company: string;
- industryOccupation: string;
- brand: string;
- revenuePerYear: string;
- logo: string;
-}
-
-export default function CompanyInfo({ company, industryOccupation, brand, revenuePerYear, logo }: CompanyInfoProps) {
- return (
-
-
-
-
- {company}
-
-
-
-
-
- {industryOccupation}
-
-
-
-
-
- {brand}
-
-
-
-
-
- {revenuePerYear} 원
-
-
-
-
-
-
- );
-}
-
-const CompanyFlex = styled(Flex)`
- ${responsiveStyle({
- default: {
- justifyContent: 'space-between',
- alignItems: 'center',
- gap: '100px',
- },
- tablet: {
- flexDirection: 'column',
- justifyContent: 'center',
- alignItems: 'center',
- gap: '15px',
- },
- mobile: {
- flexDirection: 'column',
- },
- })}
-`;
-
-const Logo = styled.img`
- width: 280px;
- height: auto;
-
- ${responsiveStyle({
- tablet: {
- margin: '0 auto',
- width: '50%',
- },
- mobile: {
- width: '70%',
- },
- })}
-`;
-
-const InfoFlex = styled(Flex)`
- ${responsiveStyle({
- default: {
- flexDirection: 'column',
- justifyContent: 'space-between',
- gap: '10px',
- },
- tablet: {
- alignItems: 'center',
- margin: '10px 0 20px 0',
- width: '70%',
- },
- })}
-`;
-
-const InfoGroup = styled(Flex)`
- ${responsiveStyle({
- default: {
- alignItems: 'center',
- gap: '30px',
- },
- tablet: {
- flexDirection: 'row',
- justifyContent: 'center',
- width: '80%',
- gap: '20px',
- },
- mobile: {
- flexDirection: 'column',
- alignItems: 'center',
- gap: '10px',
- },
- })}
-`;
-
-const infoStyle = {
- color: '#474C54',
- whiteSpace: 'nowrap',
-};
-
-const customButtonStyle = {
- backgroundColor: '#0A65CC',
- color: '#fff',
- borderRadius: '4px',
- whiteSpace: 'nowrap',
-};
diff --git a/src/features/employer/MyRecruitments/index.tsx b/src/features/employer/MyRecruitments/index.tsx
deleted file mode 100644
index 0ad8140..0000000
--- a/src/features/employer/MyRecruitments/index.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { responsiveStyle } from '@utils/responsive';
-import { Button, Flex, Typo } from '@/components/common';
-import styled from '@emotion/styled';
-
-interface RecruitmentProps {
- koreanTitle: string;
- area: string;
- koreanDetailedDescription: string;
-}
-
-interface RecruitmentsListProps {
- recruitmentsList: RecruitmentProps[];
-}
-
-export default function MyRecruitments({ recruitmentsList }: RecruitmentsListProps) {
- return (
-
-
-
- 내 공고글
-
-
- 총 {recruitmentsList.length}건
-
-
-
-
-
- 근무지
-
-
- 공고 제목
-
-
- {recruitmentsList.map((recruitment, index) => (
-
-
- {recruitment.area}
-
-
-
-
- {recruitment.koreanTitle}
-
-
- {recruitment.koreanDetailedDescription}
-
-
-
-
-
-
-
-
- ))}
-
-
- );
-}
-
-const tableHeaderStyle = {
- padding: '10px 25px',
- backgroundColor: '#F1F2F4',
- borderTop: '1px solid #000',
- borderBottom: '1px solid #000',
-};
-
-const tableHeaderTitleStyle = {
- color: '#474C54',
- fontWeight: 'bold',
-};
-
-const RecruitmentItem = styled(Flex)`
- align-items: center;
- border-bottom: 1px solid #e4e5e8;
- padding: 30px 25px;
-`;
-
-const recruitmentTitleStyle = {
- color: '#0A65CC',
-};
-
-const DetailsFlex = styled(Flex)`
- width: 70%;
- gap: 10px;
-
- ${responsiveStyle({
- tablet: {
- flexDirection: 'column',
- alignItems: 'center',
- gap: '15px',
- },
- })}
-`;
-
-const ButtonGroup = styled(Flex)`
- justify-content: flex-end;
- align-items: center;
- gap: 15px;
-
- ${responsiveStyle({
- tablet: {
- justifyContent: 'flex-start',
- },
- mobile: {
- flexDirection: 'column',
- gap: '10px',
- alignItems: 'stretch',
- },
- })}
-`;
-
-const customButtonStyle = {
- backgroundColor: '#F1F2F4',
- whiteSpace: 'nowrap',
-};
diff --git a/src/features/home/Employer.tsx b/src/features/home/Employer.tsx
deleted file mode 100644
index 61815c1..0000000
--- a/src/features/home/Employer.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Flex, Typo, Button } from '@/components/common';
-import Banner from './components/Banner';
-import { images } from '@pages/home/data/index.mock';
-import { responsiveStyle } from '@utils/responsive';
-
-export default function Employer() {
- return (
- <>
-
-
-
- {`사장님,\n 공고 등록은 하셨나요? 🤔`}
-
-
-
-
- >
- );
-}
diff --git a/src/features/home/RecruitmentList.tsx b/src/features/home/RecruitmentList.tsx
deleted file mode 100644
index 3ff2d0f..0000000
--- a/src/features/home/RecruitmentList.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { List, Flex } from '@/components/common';
-import RecruitmentCard from './components/RecruitmentCard/RecruitmentCard';
-import { RecruitmentItem } from '@/types';
-import { responsiveStyle } from '@utils/responsive';
-
-type Props = {
- recruitmentList: RecruitmentItem[];
-};
-
-export default function RecruitmentList({ recruitmentList }: Props) {
- return (
-
- (
-
-
-
-
-
-
-
-
- )}
- />
-
- );
-}
diff --git a/src/features/home/Worker.tsx b/src/features/home/Worker.tsx
deleted file mode 100644
index f709d60..0000000
--- a/src/features/home/Worker.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Banner from './components/Banner';
-import { images } from '@/pages/home/data/index.mock';
-
-export default function Worker() {
- return ;
-}
diff --git a/src/features/home/components/ConditionalRenderer.tsx b/src/features/home/components/ConditionalRenderer.tsx
new file mode 100644
index 0000000..dd9f460
--- /dev/null
+++ b/src/features/home/components/ConditionalRenderer.tsx
@@ -0,0 +1,9 @@
+import Employer from './Employer';
+import Worker from './Worker';
+
+export default function ConditionalRenderer() {
+ if (localStorage.getItem('userType') === 'employee') {
+ return ;
+ }
+ return ;
+}
diff --git a/src/features/home/components/Employer.tsx b/src/features/home/components/Employer.tsx
new file mode 100644
index 0000000..89b4622
--- /dev/null
+++ b/src/features/home/components/Employer.tsx
@@ -0,0 +1,36 @@
+import { Flex, Typo, Button } from '@/components/common';
+import Banner from './common/Banner';
+import { responsiveStyle } from '@utils/responsive';
+
+const defaultImage = [
+ { id: 1, imageUrl: 'https://www.v-on.kr/wp-content/uploads/2022/06/IT_twi001t2302755-1024x683.jpg' },
+];
+
+const headerStyle = responsiveStyle({
+ default: {
+ marginBottom: '40px',
+ whiteSpace: 'pre-line',
+ textAlign: 'center',
+ },
+ tablet: {
+ fontSize: '36px',
+ marginBottom: '32px',
+ },
+ mobile: {
+ fontSize: '28px',
+ marginBottom: '28px',
+ },
+});
+
+export default function Employer() {
+ return (
+
+
+
+ {`사장님,\n 공고 등록은 하셨나요? 🤔`}
+
+
+
+
+ );
+}
diff --git a/src/features/home/components/RecruitmentCard/Button.tsx b/src/features/home/components/RecruitmentCard/Button.tsx
deleted file mode 100644
index 5750f5d..0000000
--- a/src/features/home/components/RecruitmentCard/Button.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Typo, Icon, Flex } from '@components/common';
-
-export default function Detail() {
- return (
-
-
- Read More
-
-
-
- );
-}
diff --git a/src/features/home/components/RecruitmentCard/CompanyImage.tsx b/src/features/home/components/RecruitmentCard/CompanyImage.tsx
deleted file mode 100644
index d1b210f..0000000
--- a/src/features/home/components/RecruitmentCard/CompanyImage.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Image } from '@components/common';
-import { useRecruitmentCardContext } from './RecruitmentCard.context';
-import { responsiveStyle } from '@utils/responsive';
-
-export default function CompanyImage() {
- const { recruitment } = useRecruitmentCardContext();
-
- return (
-
- );
-}
diff --git a/src/features/home/components/RecruitmentCard/CompanyName.tsx b/src/features/home/components/RecruitmentCard/CompanyName.tsx
deleted file mode 100644
index fdefd9e..0000000
--- a/src/features/home/components/RecruitmentCard/CompanyName.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Typo } from '@components/common';
-import { useRecruitmentCardContext } from './RecruitmentCard.context';
-import { responsiveStyle } from '@utils/responsive';
-
-export default function CompanyName() {
- const { recruitment } = useRecruitmentCardContext();
-
- return (
-
- {recruitment.companyName}
-
- );
-}
diff --git a/src/features/home/components/RecruitmentCard/Detail.tsx b/src/features/home/components/RecruitmentCard/Detail.tsx
deleted file mode 100644
index 4b00214..0000000
--- a/src/features/home/components/RecruitmentCard/Detail.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Typo } from '@components/common';
-import { useRecruitmentCardContext } from './RecruitmentCard.context';
-import { responsiveStyle } from '@utils/responsive';
-
-export default function Detail() {
- const { recruitment } = useRecruitmentCardContext();
-
- return (
-
- {recruitment.area} / {recruitment.workHours}
-
- );
-}
diff --git a/src/features/home/components/RecruitmentCard/Title.tsx b/src/features/home/components/RecruitmentCard/Title.tsx
deleted file mode 100644
index 6efeffa..0000000
--- a/src/features/home/components/RecruitmentCard/Title.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Typo } from '@components/common';
-import { useRecruitmentCardContext } from './RecruitmentCard.context';
-import { responsiveStyle } from '@utils/responsive';
-
-export default function Title() {
- const { recruitment } = useRecruitmentCardContext();
-
- return (
-
- {recruitment.koreanTitle} / {recruitment.vietnameseTitle}
-
- );
-}
diff --git a/src/features/home/components/RecruitmentHeader.tsx b/src/features/home/components/RecruitmentHeader.tsx
deleted file mode 100644
index 7c16b52..0000000
--- a/src/features/home/components/RecruitmentHeader.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Flex, Typo } from '@components/common';
-import { responsiveStyle } from '@utils/responsive';
-
-export default function RecruitmentHeader() {
- return (
-
-
-
- 어떤 일자리를 구하시나요?
-
-
- 조건을 선택하고 원하는 일자리를 골라보세요.
-
-
-
- );
-}
diff --git a/src/features/home/components/Worker.tsx b/src/features/home/components/Worker.tsx
new file mode 100644
index 0000000..16418f3
--- /dev/null
+++ b/src/features/home/components/Worker.tsx
@@ -0,0 +1,8 @@
+import Banner from './common/Banner';
+import { useFetchSlides } from '@/apis/home/queries/useFetchSlides';
+
+export default function Worker() {
+ const { data: images } = useFetchSlides();
+
+ return ;
+}
diff --git a/src/features/home/components/Banner.tsx b/src/features/home/components/common/Banner.tsx
similarity index 70%
rename from src/features/home/components/Banner.tsx
rename to src/features/home/components/common/Banner.tsx
index c6e1188..c885434 100644
--- a/src/features/home/components/Banner.tsx
+++ b/src/features/home/components/common/Banner.tsx
@@ -4,13 +4,9 @@ import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import styled from '@emotion/styled';
-import { BannerItem } from '@/types';
+import { SlidesResponse } from '@/apis/home/types/response';
-type Props = {
- images: BannerItem[];
- isSlider?: boolean;
- children?: ReactNode;
-};
+const BANNER_SIZE_CONFIG = { width: '100%', height: '400px' };
const settings = {
infinite: true,
@@ -21,27 +17,32 @@ const settings = {
autoplaySpeed: 3000,
};
+type Props = {
+ images: SlidesResponse[];
+ isSlider?: boolean;
+ children?: ReactNode;
+};
+
export default function Banner({ images, isSlider = false, children }: Props) {
return (
-
+
{isSlider ? (
{images.map((image) => (
-
+
))}
) : (
-
+
)}
{children && {children}}
-
+
);
}
-const bannerImageSize = {
- width: '100%',
- height: '400px',
-};
+const Section = styled.section`
+ position: relative;
+`;
const ChildrenContent = styled.div`
position: absolute;
diff --git a/src/features/home/components/RecruitmentCard/RecruitmentCard.context.tsx b/src/features/home/components/common/RecruitmentCard/index.context.tsx
similarity index 83%
rename from src/features/home/components/RecruitmentCard/RecruitmentCard.context.tsx
rename to src/features/home/components/common/RecruitmentCard/index.context.tsx
index 8299f08..fc90938 100644
--- a/src/features/home/components/RecruitmentCard/RecruitmentCard.context.tsx
+++ b/src/features/home/components/common/RecruitmentCard/index.context.tsx
@@ -1,8 +1,8 @@
import { createContext, ReactNode, useContext } from 'react';
-import { RecruitmentItem } from '@/types';
+import { RecruitmentResponse } from '@/apis/home/types/response';
type RecruitmentCardContextProps = {
- recruitment: RecruitmentItem;
+ recruitment: RecruitmentResponse;
};
const RecruitmentCardContext = createContext(undefined);
@@ -16,7 +16,7 @@ export const useRecruitmentCardContext = () => {
};
interface ProviderProps {
- recruitment: RecruitmentItem;
+ recruitment: RecruitmentResponse;
children: ReactNode;
}
diff --git a/src/features/home/components/RecruitmentCard/RecruitmentCard.tsx b/src/features/home/components/common/RecruitmentCard/index.tsx
similarity index 60%
rename from src/features/home/components/RecruitmentCard/RecruitmentCard.tsx
rename to src/features/home/components/common/RecruitmentCard/index.tsx
index d1f98a8..5021d5e 100644
--- a/src/features/home/components/RecruitmentCard/RecruitmentCard.tsx
+++ b/src/features/home/components/common/RecruitmentCard/index.tsx
@@ -1,34 +1,30 @@
-import { RecruitmentItem } from '@/types';
import { bounceAnimation } from '@assets/styles/animations';
import { Card } from '@components/common';
import { ReactNode } from 'react';
-import { RecruitmentCardContextProvider } from './RecruitmentCard.context';
-import { Title, Button, CompanyName, CompanyImage, Detail, Salary } from '.';
+import { RecruitmentCardContextProvider } from './index.context';
+import { Title, Button, CompanyName, CompanyImage, Detail, Salary } from './sub-components';
import { responsiveStyle } from '@utils/responsive';
+import { RecruitmentResponse } from '@/apis/home/types/response';
+
+const recruitmentCardStyle = responsiveStyle({
+ default: {
+ padding: '24px',
+ cursor: 'pointer',
+ },
+ mobile: {
+ padding: '16px',
+ },
+});
type Props = {
- recruitment: RecruitmentItem;
+ recruitment: RecruitmentResponse;
children: ReactNode;
};
export default function RecruitmentCard({ recruitment, children }: Props) {
return (
-
+
{children}
diff --git a/src/features/home/components/common/RecruitmentCard/sub-components/Button.tsx b/src/features/home/components/common/RecruitmentCard/sub-components/Button.tsx
new file mode 100644
index 0000000..e3825fd
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentCard/sub-components/Button.tsx
@@ -0,0 +1,17 @@
+import { Typo, Icon, Flex } from '@components/common';
+
+const FLEX_GAP_CONFIG = { x: '8px' };
+const BUTTON_CONTAINER_STYLE = { cursor: 'pointer' };
+
+export default function Button() {
+ return (
+
+
+ Read More
+
+
+
+
+
+ );
+}
diff --git a/src/features/home/components/common/RecruitmentCard/sub-components/CompanyImage.tsx b/src/features/home/components/common/RecruitmentCard/sub-components/CompanyImage.tsx
new file mode 100644
index 0000000..a1c828c
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentCard/sub-components/CompanyImage.tsx
@@ -0,0 +1,25 @@
+import { Image } from '@components/common';
+import { useRecruitmentCardContext } from '../index.context';
+import { responsiveStyle } from '@utils/responsive';
+
+const IMAGE_SIZE_CONFIG = {
+ width: '350px',
+ height: '275px',
+};
+
+const companyImageStyle = responsiveStyle({
+ default: {
+ marginBottom: '24px',
+ },
+ mobile: {
+ marginBottom: '18px',
+ minWidth: '300px',
+ minHeight: '250px',
+ },
+});
+
+export default function CompanyImage() {
+ const { recruitment } = useRecruitmentCardContext();
+
+ return ;
+}
diff --git a/src/features/home/components/common/RecruitmentCard/sub-components/CompanyName.tsx b/src/features/home/components/common/RecruitmentCard/sub-components/CompanyName.tsx
new file mode 100644
index 0000000..7875f4f
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentCard/sub-components/CompanyName.tsx
@@ -0,0 +1,22 @@
+import { Typo } from '@components/common';
+import { useRecruitmentCardContext } from '../index.context';
+import { responsiveStyle } from '@utils/responsive';
+
+const companyNameStyle = responsiveStyle({
+ default: {
+ marginBottom: '4px',
+ },
+ mobile: {
+ fontSize: '12px',
+ },
+});
+
+export default function CompanyName() {
+ const { recruitment } = useRecruitmentCardContext();
+
+ return (
+
+ {recruitment.companyName}
+
+ );
+}
diff --git a/src/features/home/components/common/RecruitmentCard/sub-components/Detail.tsx b/src/features/home/components/common/RecruitmentCard/sub-components/Detail.tsx
new file mode 100644
index 0000000..26ca3e6
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentCard/sub-components/Detail.tsx
@@ -0,0 +1,22 @@
+import { Typo } from '@components/common';
+import { useRecruitmentCardContext } from '../index.context';
+import { responsiveStyle } from '@utils/responsive';
+
+const detailStyle = responsiveStyle({
+ default: {
+ marginBottom: '16px',
+ },
+ mobile: {
+ fontSize: '14px',
+ },
+});
+
+export default function Detail() {
+ const { recruitment } = useRecruitmentCardContext();
+
+ return (
+
+ {recruitment.area} / {recruitment.workHours}
+
+ );
+}
diff --git a/src/features/home/components/RecruitmentCard/Salary.tsx b/src/features/home/components/common/RecruitmentCard/sub-components/Salary.tsx
similarity index 51%
rename from src/features/home/components/RecruitmentCard/Salary.tsx
rename to src/features/home/components/common/RecruitmentCard/sub-components/Salary.tsx
index 2808423..e48d31b 100644
--- a/src/features/home/components/RecruitmentCard/Salary.tsx
+++ b/src/features/home/components/common/RecruitmentCard/sub-components/Salary.tsx
@@ -1,11 +1,13 @@
import { Typo } from '@components/common';
-import { useRecruitmentCardContext } from './RecruitmentCard.context';
+import { useRecruitmentCardContext } from '../index.context';
+
+const SALARY_STYLE = { marginBottom: '4px' };
export default function Salary() {
const { recruitment } = useRecruitmentCardContext();
return (
-
+
{recruitment.salary}
);
diff --git a/src/features/home/components/common/RecruitmentCard/sub-components/Title.tsx b/src/features/home/components/common/RecruitmentCard/sub-components/Title.tsx
new file mode 100644
index 0000000..f2eb19a
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentCard/sub-components/Title.tsx
@@ -0,0 +1,23 @@
+import { Typo } from '@components/common';
+import { useRecruitmentCardContext } from '../index.context';
+import { responsiveStyle } from '@utils/responsive';
+
+const titleStyle = responsiveStyle({
+ default: {
+ marginBottom: '12px',
+ },
+ mobile: {
+ marginBottom: '8px',
+ fontSize: '16px',
+ },
+});
+
+export default function Title() {
+ const { recruitment } = useRecruitmentCardContext();
+
+ return (
+
+ {recruitment.koreanTitle} / {recruitment.vietnameseTitle}
+
+ );
+}
diff --git a/src/features/home/components/RecruitmentCard/index.ts b/src/features/home/components/common/RecruitmentCard/sub-components/index.ts
similarity index 100%
rename from src/features/home/components/RecruitmentCard/index.ts
rename to src/features/home/components/common/RecruitmentCard/sub-components/index.ts
diff --git a/src/features/home/components/common/RecruitmentFilter.tsx b/src/features/home/components/common/RecruitmentFilter.tsx
new file mode 100644
index 0000000..300b652
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentFilter.tsx
@@ -0,0 +1,45 @@
+import theme from '@/assets/theme';
+import { Select, Icon, List } from '@/components/common';
+import useSelect from '@/components/common/Select/hooks/useSelect';
+
+const filterOptions = [
+ {
+ value: 'all',
+ text: '전체',
+ action: () => console.log('All clicked'),
+ },
+ {
+ value: 'age',
+ text: '나이',
+ action: () => console.log('Age clicked'),
+ },
+ {
+ value: 'area',
+ text: '지역',
+ action: () => console.log('Area clicked'),
+ },
+];
+
+const triggerStyle = { minWidth: '80px', fontSize: '16px', fontWeight: '700', color: theme.palettes.blue };
+
+export default function RecruitmentFilter() {
+ const { selectedOption, handleSelect } = useSelect(filterOptions[0]);
+
+ return (
+
+ } css={triggerStyle}>
+ {selectedOption.text}
+
+
+ (
+ handleSelect(option)}>
+ {option.text}
+
+ )}
+ />
+
+
+ );
+}
diff --git a/src/features/home/components/common/RecruitmentHeader/index.styles.ts b/src/features/home/components/common/RecruitmentHeader/index.styles.ts
new file mode 100644
index 0000000..cf7af1b
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentHeader/index.styles.ts
@@ -0,0 +1,33 @@
+import { responsiveStyle } from '@utils/responsive';
+
+export const flexStyle = responsiveStyle({
+ default: {
+ marginBottom: '60px',
+ },
+ mobile: {
+ marginBottom: '32px',
+ justifyContent: 'center',
+ },
+});
+
+export const divStyle = responsiveStyle({
+ mobile: {
+ textAlign: 'center',
+ },
+});
+
+export const headerTypoStyle = responsiveStyle({
+ default: {
+ marginBottom: '16px',
+ },
+ mobile: {
+ marginBottom: '12px',
+ fontSize: '28px',
+ },
+});
+
+export const subheaderTypoStyle = responsiveStyle({
+ mobile: {
+ fontSize: '18px',
+ },
+});
diff --git a/src/features/home/components/common/RecruitmentHeader/index.tsx b/src/features/home/components/common/RecruitmentHeader/index.tsx
new file mode 100644
index 0000000..f07a692
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentHeader/index.tsx
@@ -0,0 +1,17 @@
+import { Flex, Typo } from '@components/common';
+import { flexStyle, divStyle, headerTypoStyle, subheaderTypoStyle } from './index.styles';
+
+export default function RecruitmentHeader() {
+ return (
+
+
+
+ 어떤 일자리를 구하시나요?
+
+
+ 조건을 선택하고 원하는 일자리를 골라보세요.
+
+
+
+ );
+}
diff --git a/src/features/home/components/common/RecruitmentList.tsx b/src/features/home/components/common/RecruitmentList.tsx
new file mode 100644
index 0000000..bf0243f
--- /dev/null
+++ b/src/features/home/components/common/RecruitmentList.tsx
@@ -0,0 +1,40 @@
+import { List, Flex, Spinner, AsyncBoundary } from '@/components/common';
+import RecruitmentCard from './RecruitmentCard';
+import { responsiveStyle } from '@utils/responsive';
+import { useFetchRecruitments } from '@/apis/home/queries/useFetchRecruitments';
+
+const listContainerStyle = responsiveStyle({
+ default: {
+ gap: '32px',
+ },
+ tablet: {
+ gap: '26px',
+ },
+ mobile: {
+ gap: '18px',
+ },
+});
+
+export default function RecruitmentList() {
+ const { data: recruitmentList } = useFetchRecruitments();
+
+ return (
+ }>
+
+ (
+
+
+
+
+
+
+
+
+ )}
+ />
+
+
+ );
+}
diff --git a/src/features/layout/Header/components/LanguageFilter.tsx b/src/features/layout/Header/components/LanguageFilter.tsx
new file mode 100644
index 0000000..8882c5a
--- /dev/null
+++ b/src/features/layout/Header/components/LanguageFilter.tsx
@@ -0,0 +1,40 @@
+import theme from '@/assets/theme';
+import { Select, Icon, List } from '@/components/common';
+import useGlobalSelect from '@/components/common/Select/hooks/useGlobalSelect';
+
+const triggerStyle = { minWidth: '80px', fontSize: '16px', fontWeight: '700', color: theme.palettes.blue };
+
+const languageOptions = [
+ {
+ value: 'korean',
+ text: '한국어',
+ action: () => console.log('한국어'),
+ },
+ {
+ value: 'vietnamese',
+ text: '베트남어',
+ action: () => console.log('베트남어'),
+ },
+];
+
+export default function LanguageFilter() {
+ const { selectedOption, handleSelect } = useGlobalSelect(languageOptions[0]);
+
+ return (
+
+ } css={triggerStyle}>
+ {selectedOption.text}
+
+
+ (
+ handleSelect(option)}>
+ {option.text}
+
+ )}
+ />
+
+
+ );
+}
diff --git a/src/features/layout/Header/index.stories.tsx b/src/features/layout/Header/index.stories.tsx
index 82fd609..8a140e9 100644
--- a/src/features/layout/Header/index.stories.tsx
+++ b/src/features/layout/Header/index.stories.tsx
@@ -1,5 +1,6 @@
import { Meta, StoryObj } from '@storybook/react';
import Header from '.';
+import profileImage from '@assets/images/profile-image.svg';
const meta: Meta = {
title: 'features/layout/Header',
@@ -12,5 +13,5 @@ export default meta;
type Story = StoryObj;
export const Default: Story = {
- render: () => ,
+ render: () => ,
};
diff --git a/src/features/layout/Header/index.styles.ts b/src/features/layout/Header/index.styles.ts
new file mode 100644
index 0000000..5166a54
--- /dev/null
+++ b/src/features/layout/Header/index.styles.ts
@@ -0,0 +1,110 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { responsiveStyle } from '@/utils/responsive';
+import styled from '@emotion/styled';
+import LogoIcon from '@assets/images/hirehigher-logo.svg?react';
+import { css } from '@emotion/react';
+
+export const HeaderContainer = styled.header`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: ${palettes.white};
+ height: 88px;
+
+ ${responsiveStyle({
+ tablet: {
+ flexDirection: 'column',
+ height: 'auto',
+ padding: '10px',
+ },
+ mobile: {
+ padding: '5px',
+ },
+ })}
+`;
+
+export const LogoImg = styled(LogoIcon)`
+ cursor: pointer;
+ height: auto;
+
+ ${responsiveStyle({
+ tablet: {
+ width: '150px',
+ },
+ mobile: {
+ width: '160px',
+ },
+ })}
+`;
+
+export const flexStyle = css`
+ width: 100%;
+ max-width: 1300px;
+
+ ${responsiveStyle({
+ tablet: {
+ flexDirection: 'column',
+ },
+ })}
+`;
+
+export const menuIconStyle = css`
+ display: none;
+
+ svg {
+ cursor: pointer;
+ }
+
+ ${responsiveStyle({
+ mobile: {
+ display: 'flex',
+ },
+ })}
+`;
+
+type NavProps = {
+ open: boolean;
+};
+
+export const Nav = styled.nav`
+ display: flex;
+ align-items: center;
+ gap: 35px;
+ transition: all 0.3s linear;
+
+ ${responsiveStyle({
+ tablet: {
+ width: '100%',
+ justifyContent: 'center',
+ marginTop: '10px',
+ },
+ mobile: {
+ flexDirection: 'column',
+ alignItems: 'stretch',
+ },
+ })}
+
+ @media (max-width: 480px) {
+ max-height: ${({ open }) => (open ? '420px' : '0')};
+ opacity: ${({ open }) => (open ? '1' : '0')};
+ visibility: ${({ open }) => (open ? 'visible' : 'hidden')};
+ }
+`;
+
+export const commonButtonStyle = {
+ whiteSpace: 'nowrap',
+ borderRadius: '4px',
+};
+
+export const imageStyle = css`
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ cursor: pointer;
+`;
+
+export const customButtonStyle = {
+ ...commonButtonStyle,
+ backgroundColor: `${palettes.blue}`,
+ color: `${palettes.white}`,
+};
diff --git a/src/features/layout/Header/index.tsx b/src/features/layout/Header/index.tsx
index 1649f15..e459eeb 100644
--- a/src/features/layout/Header/index.tsx
+++ b/src/features/layout/Header/index.tsx
@@ -1,145 +1,44 @@
-import styled from '@emotion/styled';
-import LogoIcon from '@assets/images/hirehigher-logo.svg?react';
import CloseIcon from '@assets/icons/navigation/menu-close.svg?react';
import MenuIcon from '@assets/icons/navigation/menu-open.svg?react';
import Button from '@components/common/Button';
-import { Flex } from '@/components/common';
-import { responsiveStyle } from '@utils/responsive';
+import { Flex, Image } from '@/components/common';
import useToggle from '@/hooks/useToggle';
-
-interface NavProps {
- open: boolean;
-}
-
-export default function Header() {
+import LanguageFilter from './components/LanguageFilter';
+import {
+ commonButtonStyle,
+ customButtonStyle,
+ flexStyle,
+ HeaderContainer,
+ imageStyle,
+ LogoImg,
+ menuIconStyle,
+ Nav,
+} from './index.styles';
+import { UserData } from '@/types';
+
+type Props = Pick;
+
+export default function Header({ profileImage }: Props) {
const [menuOpen, toggleMenu] = useToggle();
return (
-
+
- {menuOpen ? : }
+
+ {menuOpen ? : }
+
-
+
);
}
-
-const HeaderContainer = styled.header`
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: #fff;
- height: 88px;
-
- ${responsiveStyle({
- tablet: {
- flexDirection: 'column',
- height: 'auto',
- padding: '10px',
- },
- mobile: {
- padding: '5px',
- },
- })}
-`;
-
-const LogoImg = styled(LogoIcon)`
- cursor: pointer;
- height: auto;
-
- ${responsiveStyle({
- tablet: {
- width: '150px',
- },
- mobile: {
- width: '160px',
- },
- })}
-`;
-
-const StyledFlex = styled(Flex)`
- justify-content: space-between;
- align-items: center;
- width: 100%;
- max-width: 1300px;
-
- ${responsiveStyle({
- tablet: {
- flexDirection: 'column',
- },
- })}
-`;
-
-const MobileMenuIcon = styled(Flex)`
- display: none;
- justify-content: flex-end;
-
- svg {
- cursor: pointer;
- }
-
- ${responsiveStyle({
- mobile: {
- display: 'flex',
- },
- })}
-`;
-
-const Nav = styled.nav`
- display: flex;
- align-items: center;
- gap: 15px;
- transition: all 0.3s linear;
-
- ${responsiveStyle({
- tablet: {
- width: '100%',
- justifyContent: 'center',
- marginTop: '10px',
- },
- mobile: {
- flexDirection: 'column',
- alignItems: 'stretch',
- },
- })}
-
- @media (max-width: 480px) {
- max-height: ${({ open }) => (open ? '420px' : '0')};
- opacity: ${({ open }) => (open ? '1' : '0')};
- visibility: ${({ open }) => (open ? 'visible' : 'hidden')};
- }
-`;
-
-const Dropdown = styled.select`
- padding: 10px 20px;
- color: #0a65cc;
- cursor: pointer;
- text-align: center;
- font-size: 16px;
- border: none;
-`;
-
-const commonButtonStyle = {
- whiteSpace: 'nowrap',
- borderRadius: '4px',
-};
-
-const customButtonStyle = {
- ...commonButtonStyle,
- backgroundColor: '#0a65cc',
- color: '#fff',
-};
diff --git a/src/features/layout/index.tsx b/src/features/layout/index.tsx
index 3a5b6f5..99550f4 100644
--- a/src/features/layout/index.tsx
+++ b/src/features/layout/index.tsx
@@ -1,11 +1,17 @@
import { ReactNode } from 'react';
import Footer from './Footer';
import Header from './Header';
+import profileImage from '@assets/images/profile-image.svg';
+
+const initialData = {
+ type: 'employer',
+ profileImage: profileImage,
+};
export default function Layout({ children }: { children: ReactNode }) {
return (
<>
-
+
{children}
>
diff --git a/src/features/recruitments/RecruitmentInfo/index.styles.ts b/src/features/recruitments/RecruitmentInfo/index.styles.ts
new file mode 100644
index 0000000..20526ce
--- /dev/null
+++ b/src/features/recruitments/RecruitmentInfo/index.styles.ts
@@ -0,0 +1,93 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+
+export const recruitmentFlexStyle = css`
+ ${responsiveStyle({
+ default: {
+ gap: '100px',
+ border: `1px solid ${palettes.borderGray}`,
+ borderRadius: '8px',
+ padding: '15px 30px',
+ },
+ tablet: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '20px 30px',
+ gap: '30px',
+ },
+ mobile: {
+ padding: '30px',
+ },
+ })}
+`;
+
+export const ImageWrapper = styled.div`
+ width: 280px;
+ height: 120px;
+`;
+
+export const imageStyle = css`
+ height: 100%;
+
+ ${responsiveStyle({
+ tablet: {
+ margin: '0 auto',
+ width: '50%',
+ },
+ mobile: {
+ width: '70%',
+ },
+ })}
+`;
+
+export const imageSize = {
+ width: '277px',
+ height: 'auto',
+};
+
+export const infoFlexStyle = css`
+ ${responsiveStyle({
+ default: {
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ gap: '10px',
+ },
+ tablet: {
+ width: '80%',
+ alignItems: 'center',
+ },
+ mobile: {
+ gap: '15px',
+ },
+ })}
+`;
+
+export const infoGroupStyle = css`
+ ${responsiveStyle({
+ default: {
+ alignItems: 'center',
+ gap: '30px',
+ },
+ tablet: {
+ width: '80%',
+ gap: '20px',
+ justifyContent: 'center',
+ },
+ mobile: {
+ flexDirection: 'column',
+ gap: '10px',
+ },
+ })}
+`;
+
+export const buttonStyle = css`
+ background-color: ${palettes.blue};
+`;
+
+export const buttonTextStyle = {
+ fontWeight: '600',
+ whiteSpace: 'nowrap',
+};
diff --git a/src/features/recruitments/RecruitmentInfo/index.tsx b/src/features/recruitments/RecruitmentInfo/index.tsx
new file mode 100644
index 0000000..5918801
--- /dev/null
+++ b/src/features/recruitments/RecruitmentInfo/index.tsx
@@ -0,0 +1,50 @@
+import { Button, Flex, Icon, Image, Typo } from '@/components/common';
+import AreaIcon from '@assets/icons/recruitmentInfo/area.svg?react';
+import SalaryIcon from '@assets/icons/recruitmentInfo/salary.svg?react';
+import { RecruitmentItem } from '@/types';
+import {
+ buttonStyle,
+ buttonTextStyle,
+ imageSize,
+ imageStyle,
+ ImageWrapper,
+ infoFlexStyle,
+ infoGroupStyle,
+ recruitmentFlexStyle,
+} from './index.styles';
+
+type Props = Pick;
+
+export default function RecruitmentInfo({ image, companyName, koreanTitle, area, salary }: Props) {
+ return (
+
+
+
+
+
+ {companyName}
+
+ {koreanTitle}
+
+
+
+
+ {area}
+
+
+
+ 시급 {salary}
+
+
+
+
+
+
+ 자세히 보러가기
+
+
+
+
+
+ );
+}
diff --git a/src/features/recruitments/RecruitmentList/RecruitmentsTable/index.styles.ts b/src/features/recruitments/RecruitmentList/RecruitmentsTable/index.styles.ts
new file mode 100644
index 0000000..5b9a678
--- /dev/null
+++ b/src/features/recruitments/RecruitmentList/RecruitmentsTable/index.styles.ts
@@ -0,0 +1,47 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+
+export const recruitmentTitleStyle = {
+ color: `${palettes.blue}`,
+};
+
+export const recruitmentStyle = css`
+ ${responsiveStyle({
+ default: {
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: '20px 0',
+ },
+ tablet: {
+ flexDirection: 'column',
+ gap: '15px',
+ padding: '15px 0',
+ },
+ mobile: {
+ padding: '10px 0',
+ },
+ })}
+`;
+
+export const buttonGroupStyle = css`
+ ${responsiveStyle({
+ default: {
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ gap: '15px',
+ },
+ tablet: {
+ justifyContent: 'flex-start',
+ },
+ mobile: {
+ flexDirection: 'column',
+ alignItems: 'stretch',
+ },
+ })}
+`;
+
+export const buttonStyle = css`
+ background-color: ${palettes.backgroundGray};
+ white-space: nowrap;
+`;
diff --git a/src/features/recruitments/RecruitmentList/RecruitmentsTable/index.tsx b/src/features/recruitments/RecruitmentList/RecruitmentsTable/index.tsx
new file mode 100644
index 0000000..fe906fe
--- /dev/null
+++ b/src/features/recruitments/RecruitmentList/RecruitmentsTable/index.tsx
@@ -0,0 +1,46 @@
+import { Button, Flex, List, Table, Td, Th, Typo } from '@/components/common';
+import { RecruitmentItem } from '@/types';
+import { buttonGroupStyle, buttonStyle, recruitmentStyle, recruitmentTitleStyle } from './index.styles';
+
+type Props = {
+ recruitmentList: RecruitmentItem[];
+};
+
+export default function RecruitmentsTable({ recruitmentList }: Props) {
+ return (
+
+
+
+ 근무지 |
+ 공고 제목 |
+
+
+
+ (
+
+ {recruitment.area} |
+
+
+
+
+ {recruitment.companyName}
+
+
+ {recruitment.koreanTitle}
+
+
+
+ 지원자 보러가기
+ 마감하기
+
+
+ |
+
+ )}
+ />
+
+
+ );
+}
diff --git a/src/features/recruitments/RecruitmentList/index.tsx b/src/features/recruitments/RecruitmentList/index.tsx
new file mode 100644
index 0000000..cb3ccdd
--- /dev/null
+++ b/src/features/recruitments/RecruitmentList/index.tsx
@@ -0,0 +1,23 @@
+import { Flex, Typo } from '@/components/common';
+import RecruitmentsTable from './RecruitmentsTable';
+import { RecruitmentItem } from '@/types';
+
+type Props = {
+ recruitmentList: RecruitmentItem[];
+};
+
+export default function RecruitmentList({ recruitmentList }: Props) {
+ return (
+
+
+
+ 내 공고글
+
+
+ 총 {recruitmentList.length}건
+
+
+
+
+ );
+}
diff --git a/src/features/visaRegistration/index.styles.ts b/src/features/visaRegistration/index.styles.ts
new file mode 100644
index 0000000..fa38d2a
--- /dev/null
+++ b/src/features/visaRegistration/index.styles.ts
@@ -0,0 +1,41 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+
+export const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ font-size: 16px;
+ font-weight: bold;
+ width: 100%;
+ max-width: 700px;
+ margin: 0 auto;
+
+ ${responsiveStyle({
+ tablet: {
+ gap: '20px',
+ padding: '0 20px',
+ },
+ mobile: {
+ padding: '0 15px',
+ },
+ })}
+`;
+
+export const inputStyle = css`
+ padding: 15px 20px;
+ width: 100%;
+`;
+
+export const buttonStyle = css`
+ background-color: ${palettes.blue};
+ color: ${palettes.white};
+ border-radius: 4px;
+`;
+
+export const ErrorMessage = styled.div`
+ color: red;
+ font-size: 13px;
+`;
diff --git a/src/features/employee/visaRegistration/index.tsx b/src/features/visaRegistration/index.tsx
similarity index 63%
rename from src/features/employee/visaRegistration/index.tsx
rename to src/features/visaRegistration/index.tsx
index c3e29c5..e9ac8da 100644
--- a/src/features/employee/visaRegistration/index.tsx
+++ b/src/features/visaRegistration/index.tsx
@@ -1,28 +1,21 @@
-import { responsiveStyle } from '@utils/responsive';
import { Button, Flex, Input, Modal } from '@/components/common';
-import styled from '@emotion/styled';
-import { ChangeEvent, useState } from 'react';
+import { ChangeEvent, useMemo, useState } from 'react';
+import { buttonStyle, ErrorMessage, Form, inputStyle } from './index.styles';
+import { validateForeignerNumber } from './validateForeignerNumber';
export default function VisaRegistrationForm() {
const [foreignerNumber, setForeignerNumber] = useState('');
const [visaGenerateDate, setVisaGenerateDate] = useState('');
const [error, setError] = useState('');
- const [isFormValid, setIsFormValid] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
-
- const validateForeignerNumber = (number: string) => {
- const regex = /^\d{6}-\d{7}$/;
- return regex.test(number);
- };
+ const formValid = useMemo(() => !error, [error]);
const handleForeignerNumberChange = (e: ChangeEvent) => {
const { value } = e.target;
if (!validateForeignerNumber(value) && value !== '') {
setError('올바른 형식으로 입력해주세요. (형식: 000000-0000000)');
- setIsFormValid(false);
} else {
setError('');
- setIsFormValid(true);
}
setForeignerNumber(value);
};
@@ -45,7 +38,7 @@ export default function VisaRegistrationForm() {
type="text"
value={foreignerNumber}
onChange={handleForeignerNumberChange}
- style={inputStyle}
+ css={inputStyle}
required
/>
{error && {error}}
@@ -56,12 +49,12 @@ export default function VisaRegistrationForm() {
type="date"
value={visaGenerateDate}
onChange={(e) => setVisaGenerateDate(e.target.value)}
- style={inputStyle}
+ css={inputStyle}
required
/>
-
+
등록하기
@@ -70,46 +63,10 @@ export default function VisaRegistrationForm() {
확인}
+ /* onClose 부분 추후 수정 예정 */
onClose={closeModal}
/>
)}
>
);
}
-
-const Form = styled.form`
- display: flex;
- flex-direction: column;
- gap: 40px;
- font-size: 16px;
- font-weight: bold;
- width: 100%;
- max-width: 700px;
- margin: 0 auto;
-
- ${responsiveStyle({
- tablet: {
- gap: '20px',
- padding: '0 20px',
- },
- mobile: {
- padding: '0 15px',
- },
- })}
-`;
-
-const inputStyle = {
- padding: '15px 20px',
- width: '100%',
-};
-
-const buttonStyle = {
- backgroundColor: '#0A65CC',
- color: '#fff',
- borderRadius: '4px',
-};
-
-const ErrorMessage = styled.div`
- color: red;
- font-size: 13px;
-`;
diff --git a/src/features/visaRegistration/validateForeignerNumber.ts b/src/features/visaRegistration/validateForeignerNumber.ts
new file mode 100644
index 0000000..56a901a
--- /dev/null
+++ b/src/features/visaRegistration/validateForeignerNumber.ts
@@ -0,0 +1,4 @@
+export const validateForeignerNumber = (number: string) => {
+ const regex = /^\d{6}-\d{7}$/;
+ return regex.test(number);
+};
diff --git a/src/main.tsx b/src/main.tsx
index 65bb32c..cd82fd2 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,5 +1,8 @@
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router.tsx';
+import { setupMockServiceWorker } from './mocks/setupMockServiceWorker.ts';
+
+setupMockServiceWorker();
createRoot(document.getElementById('root')!).render();
diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts
new file mode 100644
index 0000000..0a56427
--- /dev/null
+++ b/src/mocks/browser.ts
@@ -0,0 +1,4 @@
+import { setupWorker } from 'msw/browser';
+import { handlers } from './handlers';
+
+export const worker = setupWorker(...handlers);
diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts
new file mode 100644
index 0000000..d6cdae5
--- /dev/null
+++ b/src/mocks/handlers.ts
@@ -0,0 +1,11 @@
+import { recruitmentsMockHandler } from '@/apis/home/mocks/recruitmentsMockHandler';
+import { slidesMockHandler } from '@/apis/home/mocks/slidesMockHandler';
+import { EmployeePageMockHandler } from '@/apis/employee/mock/getMyApplication.mock';
+import { noticesMockHandler } from '@/apis/employer/mock/postNotice.mock';
+
+export const handlers = [
+ ...recruitmentsMockHandler,
+ ...slidesMockHandler,
+ ...noticesMockHandler,
+ ...EmployeePageMockHandler,
+];
diff --git a/src/mocks/index.ts b/src/mocks/index.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/mocks/setupMockServiceWorker.ts b/src/mocks/setupMockServiceWorker.ts
new file mode 100644
index 0000000..2130336
--- /dev/null
+++ b/src/mocks/setupMockServiceWorker.ts
@@ -0,0 +1,6 @@
+export async function setupMockServiceWorker() {
+ if (process.env.NODE_ENV === 'development') {
+ const { worker } = await import('./browser');
+ worker.start();
+ }
+}
diff --git a/src/pages/applicants/index.mock.ts b/src/pages/applicants/index.mock.ts
new file mode 100644
index 0000000..bb5d07f
--- /dev/null
+++ b/src/pages/applicants/index.mock.ts
@@ -0,0 +1,28 @@
+import CompanyImage from '@assets/images/coupang.png';
+
+export const recruitment = {
+ image: CompanyImage,
+ companyName: '쿠팡 유성점',
+ koreanTitle: '쿠팡 유성점에서 아르바이트 모집합니다.',
+ area: '대전 유성구',
+ salary: 50000000,
+};
+
+export const applicantList = [
+ {
+ userId: 1,
+ name: '이름1',
+ resumeId: 1,
+ applyId: 1,
+ applicantNation: '베트남',
+ korean: '중급',
+ },
+ {
+ userId: 2,
+ name: '이름2',
+ resumeId: 2,
+ applyId: 2,
+ applicantNation: '베트남',
+ korean: '고급',
+ },
+];
diff --git a/src/pages/applicants/index.stories.tsx b/src/pages/applicants/index.stories.tsx
new file mode 100644
index 0000000..3182219
--- /dev/null
+++ b/src/pages/applicants/index.stories.tsx
@@ -0,0 +1,14 @@
+import { Meta, StoryObj } from '@storybook/react';
+import Applicants from '.';
+
+const meta: Meta = {
+ title: 'pages/Applicants',
+ component: Applicants,
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/pages/applicants/index.tsx b/src/pages/applicants/index.tsx
new file mode 100644
index 0000000..226e1e7
--- /dev/null
+++ b/src/pages/applicants/index.tsx
@@ -0,0 +1,31 @@
+import { Flex, InnerContainer } from '@/components/common';
+import ApplicantList from '@/features/applicants/ApplicantList';
+import RecruitmentsInfo from '@/features/recruitments/RecruitmentInfo';
+import Layout from '@/features/layout';
+import styled from '@emotion/styled';
+import { applicantList, recruitment } from './index.mock';
+
+export default function Applicants() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const MainContainer = styled.div`
+ padding: 40px 0 80px 0;
+`;
diff --git a/src/pages/apply/applyguide/ApplyGuide.stories.tsx b/src/pages/apply/applyguide/ApplyGuide.stories.tsx
index b327357..c0a470b 100644
--- a/src/pages/apply/applyguide/ApplyGuide.stories.tsx
+++ b/src/pages/apply/applyguide/ApplyGuide.stories.tsx
@@ -1,18 +1,10 @@
import { Meta, StoryObj } from '@storybook/react';
import ApplyGuide from './ApplyGuide';
-import { BrowserRouter } from 'react-router-dom';
const meta: Meta = {
title: 'PAGES/Apply/ApplyGuide',
component: ApplyGuide,
tags: ['autodocs'],
- decorators: [
- (Story) => (
-
-
-
- ),
- ],
};
export default meta;
diff --git a/src/pages/auth/Loading/index.tsx b/src/pages/auth/Loading/index.tsx
new file mode 100644
index 0000000..7502e1c
--- /dev/null
+++ b/src/pages/auth/Loading/index.tsx
@@ -0,0 +1,18 @@
+import { Spinner, Typo } from '@/components/common';
+import { useGoogleOAuth } from '@/apis/auth/mutations/useGoogleOAuth';
+
+export default function LoadingPage() {
+ const { isLoading } = useGoogleOAuth();
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+ 로그인 처리 중 오류가 발생했습니다.
+
+ )}
+
+ );
+}
diff --git a/src/pages/auth/SignIn/index.styles.ts b/src/pages/auth/SignIn/index.styles.ts
new file mode 100644
index 0000000..a5e045d
--- /dev/null
+++ b/src/pages/auth/SignIn/index.styles.ts
@@ -0,0 +1,20 @@
+import theme from '@/assets/theme';
+import { responsiveStyle, responsiveSectionPadding } from '@/utils/responsive';
+
+export const sectionStyle = {
+ backgroundColor: theme.palettes.backgroundGray,
+ ...responsiveStyle(responsiveSectionPadding),
+};
+
+export const innerContainerStyle = responsiveStyle({
+ mobile: {
+ flexDirection: 'column',
+ },
+});
+
+export const catchphraseContainerStyle = responsiveStyle({
+ default: {
+ marginRight: '24px',
+ },
+ mobile: { marginRight: '0', alignItems: 'center', marginBottom: '32px' },
+});
diff --git a/src/pages/auth/SignIn/index.tsx b/src/pages/auth/SignIn/index.tsx
index 0bb9029..51b1e5c 100644
--- a/src/pages/auth/SignIn/index.tsx
+++ b/src/pages/auth/SignIn/index.tsx
@@ -1,83 +1,19 @@
-import { InnerContainer, Flex, Typo, Icon, Button } from '@components/common';
+import { InnerContainer, Flex } from '@components/common';
import Layout from '@features/layout';
-import theme from '@/assets/theme';
import Illustration from '@assets/images/signin-Illustration.svg?react';
-import { responsiveSectionPadding, responsiveStyle } from '@utils/responsive';
+import { sectionStyle, innerContainerStyle, catchphraseContainerStyle } from './index.styles';
+import { SignInText } from '@/features/auth/SignIn/components/SignInText';
+import { SignInButton } from '@/features/auth/SignIn/components/SignInButton';
export default function SignIn() {
return (
-
+
-
-
-
-
- 지금 바로 시작하세요. 🚀
-
-
- 안정적이고 투명한 고용 관계의 시작, 지금 바로 경험해보세요!
-
-
-
-
-
-
- Sign up with Google
-
-
-
+
+
+
+
diff --git a/src/pages/auth/SignUp/index.tsx b/src/pages/auth/SignUp/index.tsx
index 3ae46c0..fec169a 100644
--- a/src/pages/auth/SignUp/index.tsx
+++ b/src/pages/auth/SignUp/index.tsx
@@ -1,68 +1,20 @@
-import { ReactNode, useState } from 'react';
import Layout from '@features/layout';
-import { Flex, Typo, InnerContainer } from '@components/common';
+import { InnerContainer } from '@components/common';
import { responsiveStyle, responsiveSectionPadding } from '@utils/responsive';
-import RoleSelection from '@/features/auth/RoleSelection';
-import RoleModal from '@/features/auth/RoleModal';
-import useToggle from '@hooks/useToggle';
+import RoleSelection from '@/features/auth/SignUp/components/RoleSelection';
+import SignUpText from '@/features/auth/SignUp/components/SignUpText';
-export default function SignUp() {
- const [isToggle, toggle] = useToggle();
- const [modalContent, setModalContent] = useState();
-
- const handleRoleSelect = (modalContent: ReactNode) => {
- toggle();
- setModalContent(modalContent);
- };
+const sectionStyle = responsiveStyle(responsiveSectionPadding);
+export default function SignUp() {
return (
-
+
-
-
- 가입자 정보 선택
-
-
- 대상에 해당하는 가입자 정보를 선택해주세요.
-
-
-
+
+
- {isToggle && }
);
}
diff --git a/src/pages/auth/index.ts b/src/pages/auth/index.ts
deleted file mode 100644
index 46ddd03..0000000
--- a/src/pages/auth/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as SignIn } from './SignIn';
-export { default as SignUp } from './SignUp';
diff --git a/src/pages/contract/EmployeeContract/EmployeeContract.tsx b/src/pages/contract/EmployeeContract/EmployeeContract.tsx
index 7913f30..6cdac37 100644
--- a/src/pages/contract/EmployeeContract/EmployeeContract.tsx
+++ b/src/pages/contract/EmployeeContract/EmployeeContract.tsx
@@ -4,154 +4,152 @@ import styled from '@emotion/styled';
export const EmployeeContract = () => {
return (
- <>
-
-
+
);
};
diff --git a/src/pages/contract/EmployerContract/EmployerContract.tsx b/src/pages/contract/EmployerContract/EmployerContract.tsx
index 24f0eef..ce8328e 100644
--- a/src/pages/contract/EmployerContract/EmployerContract.tsx
+++ b/src/pages/contract/EmployerContract/EmployerContract.tsx
@@ -4,157 +4,153 @@ import styled from '@emotion/styled';
export const EmployerContract = () => {
return (
- <>
-
-
+
);
};
diff --git a/src/pages/employee/myPage/EmployeeMyPage.stories.tsx b/src/pages/employee/myPage/EmployeeMyPage.stories.tsx
new file mode 100644
index 0000000..5cdaa9a
--- /dev/null
+++ b/src/pages/employee/myPage/EmployeeMyPage.stories.tsx
@@ -0,0 +1,14 @@
+import { Meta, StoryObj } from '@storybook/react';
+import EmployeeMyPage from './EmployeeMyPage';
+
+const meta: Meta = {
+ title: 'pages/EmployeeMyPage',
+ component: EmployeeMyPage,
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/pages/employee/myPage/EmployeeMyPage.tsx b/src/pages/employee/myPage/EmployeeMyPage.tsx
new file mode 100644
index 0000000..de02c3f
--- /dev/null
+++ b/src/pages/employee/myPage/EmployeeMyPage.tsx
@@ -0,0 +1,55 @@
+import { Icon, InnerContainer, Typo } from '@/components/common';
+import Layout from '@/features/layout';
+import styled from '@emotion/styled';
+import MyRecruitList from '../../../features/employee/myPage/MyRecruitList';
+import CardButton from '../../../features/employee/myPage/CardButton';
+import EmployeeProfile from '../../../features/employee/myPage/EmployeeProfile';
+// import { myRecruitList } from './data/index.mock';
+import { useGetMyApplication } from '@/apis/employee/hooks/useGetMyApplication';
+
+export default function EmployeeMyPage() {
+ const { data: myRecruitList } = useGetMyApplication();
+
+ return (
+
+
+
+
+
+
+ 이력서 등록
+
+
+
+ 사인 등록
+
+
+
+ 외국인 번호 및 비자 발급 일자 등록
+
+
+
+
+
+
+ 내가 지원한 공고
+
+ {myRecruitList && }
+
+
+
+ );
+}
+
+const Section = styled.div`
+ width: 100%;
+ direction: column;
+ align-items: center;
+ margin-bottom: 52px;
+`;
+
+const ColumnSection = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+`;
diff --git a/src/pages/employee/myPage/data/index.mock.ts b/src/pages/employee/myPage/data/index.mock.ts
new file mode 100644
index 0000000..8a82202
--- /dev/null
+++ b/src/pages/employee/myPage/data/index.mock.ts
@@ -0,0 +1,36 @@
+import { MyRecruitListProps } from '@/types';
+
+export const myRecruitList: MyRecruitListProps[] = [
+ {
+ id: 1,
+ image:
+ 'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
+ title: '제목',
+ area: '대전광역시 유성구',
+ state: '근로계약서 서명하기',
+ },
+ {
+ id: 2,
+ image:
+ 'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
+ title: '제목2',
+ area: '대전광역시 유성구',
+ state: '채용 마감',
+ },
+ {
+ id: 1,
+ image:
+ 'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
+ title: '제목3',
+ area: '대전광역시 유성구',
+ state: '지원서 검토중',
+ },
+ {
+ id: 1,
+ image:
+ 'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
+ title: '제목4',
+ area: '대전광역시 유성구',
+ state: '채용 완료',
+ },
+];
diff --git a/src/pages/employee/visaRegistration/index.tsx b/src/pages/employee/visaRegistration/index.tsx
deleted file mode 100644
index 4258d59..0000000
--- a/src/pages/employee/visaRegistration/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { responsiveStyle } from '@utils/responsive';
-import { Flex, InnerContainer } from '@/components/common';
-import Layout from '@/features/layout';
-import VisaRegistrationForm from '@/features/employee/visaRegistration';
-import styled from '@emotion/styled';
-
-export default function VisaRegistration() {
- return (
-
-
-
-
-
- 외국인 번호 및 비자 발급 일자 등록
-
-
-
-
-
-
- );
-}
-
-const MainContainer = styled.div`
- padding: 40px 0;
-`;
-
-const innerContainerStyle = {
- height: '520px',
- padding: '50px 0',
- border: '1px solid #E9E9E9',
- borderRadius: '12px',
-};
-
-const Title = styled.div`
- font-size: 24px;
- font-weight: bold;
- text-align: center;
-`;
-
-const BreakableText = styled.span`
- ${responsiveStyle({
- default: {
- display: 'inline',
- },
- mobile: {
- display: 'block',
- },
- })}
-`;
diff --git a/src/pages/employer/myAccount/index.tsx b/src/pages/employer/myAccount/index.tsx
deleted file mode 100644
index 2b66591..0000000
--- a/src/pages/employer/myAccount/index.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import Layout from '@/features/layout';
-import CompanyLogo from '@/features/employer/CompanyInfo/coupang.png';
-import CompanyInfo from '@/features/employer/CompanyInfo';
-import styled from '@emotion/styled';
-import { Flex, InnerContainer } from '@/components/common';
-import MyRecruitments from '@/features/employer/MyRecruitments';
-
-const initialCompanyData = {
- company: '쿠팡 유성점',
- industryOccupation: '온라인 소매',
- brand: '쿠팡',
- revenuePerYear: '15조',
- logo: CompanyLogo,
-};
-
-const initialRecruitmentsData = [
- {
- koreanTitle: '제목1',
- area: '대전 유성구',
- koreanDetailedDescription: '쿠팡 유성점에서 아르바이트 모집합니다.',
- },
- {
- koreanTitle: '제목2',
- area: '대전 유성구',
- koreanDetailedDescription: '쿠팡 유성점에서 아르바이트 모집합니다.',
- },
- {
- koreanTitle: '제목3',
- area: '대전 유성구',
- koreanDetailedDescription: '쿠팡 유성점에서 아르바이트 모집합니다.',
- },
-];
-
-export default function EmployerMyAccount() {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-const MainContainer = styled.div`
- padding: 60px 0;
-`;
diff --git a/src/pages/employer/postNotice/PostNotice.tsx b/src/pages/employer/postNotice/PostNotice.tsx
index 289b79e..3b2f6a2 100644
--- a/src/pages/employer/postNotice/PostNotice.tsx
+++ b/src/pages/employer/postNotice/PostNotice.tsx
@@ -1,50 +1,75 @@
+import { FetchPostNotice } from '@/apis/employer/hooks/usePostNotice';
import { Button, Flex, Input, Typo } from '@/components/common';
import Layout from '@/features/layout';
import styled from '@emotion/styled';
+import { useState } from 'react';
+
+export default function PostNotice() {
+ const mutation = FetchPostNotice();
+ const [inputs, setInputs] = useState({
+ salary: '',
+ workingDuration: '',
+ workDays: '',
+ workHours: '',
+ workType: '',
+ eligibilityCriteria: '',
+ preferredConditions: '',
+ });
+
+ const { salary, workingDuration, workDays, workHours, workType, eligibilityCriteria, preferredConditions } = inputs;
+
+ const onChange = (e: React.ChangeEvent) => {
+ const { value, name } = e.target;
+ setInputs({
+ ...inputs,
+ [name]: value,
+ });
+ };
+
+ const handlePostNotice = () => {
+ mutation.mutate(inputs);
+ };
-const PostNotice = () => {
return (
- <>
-
-
-
- >
+
+
+
);
-};
+}
const LineWrapper = styled.div`
border: 1px solid #e9e9e9;
@@ -59,4 +84,4 @@ const InputContainer = styled.div`
margin-top: 32px;
`;
-export default PostNotice;
+const InputStyle = { width: '700px', height: '48px', marginTop: '12px' };
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index 61143ca..0a162f0 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -1,21 +1,24 @@
-import { InnerContainer } from '@components/common';
+import { InnerContainer, Flex } from '@components/common';
import { responsiveStyle, responsiveSectionPadding } from '@utils/responsive';
import Layout from '@features/layout';
-import { recruitmentList } from './data/index.mock';
-// import Worker from '@features/home/Worker';
-import Employer from '@features/home/Employer';
-import RecruitmentHeader from '@features/home/components/RecruitmentHeader';
-import RecruitmentList from '@features/home/RecruitmentList';
+import RecruitmentHeader from '@features/home/components/common/RecruitmentHeader';
+import RecruitmentFilter from '@features/home/components/common/RecruitmentFilter';
+import RecruitmentList from '@features/home/components/common/RecruitmentList';
+import ConditionalRenderer from '@features/home/components/ConditionalRenderer';
+
+const sectionStyle = responsiveStyle(responsiveSectionPadding);
export default function Home() {
return (
- {/* */}
-
-
diff --git a/src/pages/myAccount/employer/index.mock.ts b/src/pages/myAccount/employer/index.mock.ts
new file mode 100644
index 0000000..85a5905
--- /dev/null
+++ b/src/pages/myAccount/employer/index.mock.ts
@@ -0,0 +1,20 @@
+import LogoImage from '@assets/images/coupang.png';
+
+export const companyList = [
+ {
+ id: 1,
+ name: '쿠팡 유성점 1',
+ industryOccupation: '온라인 소매',
+ brand: '쿠팡',
+ revenuePerYear: 1000000,
+ logoImage: LogoImage,
+ },
+ {
+ id: 2,
+ name: '쿠팡 유성점 2',
+ industryOccupation: '온라인 소매',
+ brand: '쿠팡',
+ revenuePerYear: 1000000,
+ logoImage: LogoImage,
+ },
+];
diff --git a/src/pages/employer/myAccount/index.stories.tsx b/src/pages/myAccount/employer/index.stories.tsx
similarity index 73%
rename from src/pages/employer/myAccount/index.stories.tsx
rename to src/pages/myAccount/employer/index.stories.tsx
index a89a309..3f5ba5d 100644
--- a/src/pages/employer/myAccount/index.stories.tsx
+++ b/src/pages/myAccount/employer/index.stories.tsx
@@ -1,8 +1,8 @@
import { Meta, StoryObj } from '@storybook/react';
-import EmployerMyAccount from '../../employer/myAccount';
+import EmployerMyAccount from '.';
const meta: Meta = {
- title: 'pages/Employer/MyAccount',
+ title: 'pages/MyAccount/Employer',
component: EmployerMyAccount,
tags: ['autodocs'],
};
diff --git a/src/pages/myAccount/employer/index.styles.ts b/src/pages/myAccount/employer/index.styles.ts
new file mode 100644
index 0000000..beb492d
--- /dev/null
+++ b/src/pages/myAccount/employer/index.styles.ts
@@ -0,0 +1,30 @@
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+
+export const innerContainerStyle = css`
+ ${responsiveStyle({
+ default: {
+ padding: '60px 0 80px 0',
+ },
+ tablet: {
+ padding: '60px 0 80px 0',
+ },
+ mobile: {
+ padding: '40px 0 80px 0',
+ },
+ })}
+`;
+
+export const typoStyle = {
+ ...responsiveStyle({
+ default: {
+ fontWeight: 'bold',
+ },
+ tablet: {
+ fontSize: '33px',
+ },
+ mobile: {
+ fontSize: '30px',
+ },
+ }),
+};
diff --git a/src/pages/myAccount/employer/index.tsx b/src/pages/myAccount/employer/index.tsx
new file mode 100644
index 0000000..84af857
--- /dev/null
+++ b/src/pages/myAccount/employer/index.tsx
@@ -0,0 +1,22 @@
+import { Flex, InnerContainer, Typo } from '@/components/common';
+import Layout from '@/features/layout';
+import CompanyList from '@/features/companies/CompanyList';
+import { companyList } from './index.mock';
+import { innerContainerStyle, typoStyle } from './index.styles';
+
+export default function EmployerMyAccount() {
+ return (
+
+
+
+
+
+ 사장님, 안녕하세요!
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/myCompany/index.mock.ts b/src/pages/myCompany/index.mock.ts
new file mode 100644
index 0000000..6bae7d6
--- /dev/null
+++ b/src/pages/myCompany/index.mock.ts
@@ -0,0 +1,42 @@
+import CompanyLogo from '@assets/images/coupang.png';
+
+export const company = {
+ name: '쿠팡 유성점',
+ industryOccupation: '온라인 소매',
+ brand: '쿠팡',
+ revenuePerYear: 10000000,
+ logoImage: CompanyLogo,
+};
+
+export const recruitmentList = [
+ {
+ recruitmentId: 1,
+ image: CompanyLogo,
+ koreanTitle: '쿠팡 유성점에서 아르바이트 모집합니다.',
+ vietnameseTitle: '',
+ companyName: '쿠팡 유성점',
+ salary: 100000,
+ area: '대전 유성구',
+ workHours: '',
+ },
+ {
+ recruitmentId: 2,
+ image: CompanyLogo,
+ koreanTitle: '쿠팡 유성점에서 아르바이트 모집합니다.',
+ vietnameseTitle: '',
+ companyName: '쿠팡 유성점',
+ salary: 100000,
+ area: '대전 유성구',
+ workHours: '',
+ },
+ {
+ recruitmentId: 3,
+ image: CompanyLogo,
+ koreanTitle: '쿠팡 유성점에서 아르바이트 모집합니다.',
+ vietnameseTitle: '',
+ companyName: '쿠팡 유성점',
+ salary: 100000,
+ area: '대전 유성구',
+ workHours: '',
+ },
+];
diff --git a/src/pages/myCompany/index.stories.tsx b/src/pages/myCompany/index.stories.tsx
new file mode 100644
index 0000000..96fdee8
--- /dev/null
+++ b/src/pages/myCompany/index.stories.tsx
@@ -0,0 +1,14 @@
+import { Meta, StoryObj } from '@storybook/react';
+import MyCompany from '.';
+
+const meta: Meta = {
+ title: 'pages/MyCompany',
+ component: MyCompany,
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/pages/myCompany/index.styles.ts b/src/pages/myCompany/index.styles.ts
new file mode 100644
index 0000000..597708d
--- /dev/null
+++ b/src/pages/myCompany/index.styles.ts
@@ -0,0 +1,55 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+
+export const innerContainerStyle = css`
+ ${responsiveStyle({
+ default: {
+ padding: '60px 0 80px 0',
+ },
+ tablet: {
+ padding: '60px 0 80px 0',
+ },
+ mobile: {
+ padding: '40px 0 80px 0',
+ },
+ })}
+`;
+
+export const companyWrapperStyle = css`
+ ${responsiveStyle({
+ tablet: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: '15px',
+ width: '100%',
+ },
+ mobile: {
+ flexDirection: 'column',
+ },
+ })}
+`;
+
+export const imageStyle = css`
+ ${responsiveStyle({
+ default: {
+ width: '280px',
+ height: 'auto',
+ },
+ tablet: {
+ margin: '0 auto',
+ width: '50%',
+ },
+ mobile: {
+ width: '70%',
+ },
+ })}
+`;
+
+export const buttonStyle = css`
+ background-color: ${palettes.blue};
+ color: ${palettes.white};
+ border-radius: 4px;
+ white-space: nowrap;
+`;
diff --git a/src/pages/myCompany/index.tsx b/src/pages/myCompany/index.tsx
new file mode 100644
index 0000000..6baacd0
--- /dev/null
+++ b/src/pages/myCompany/index.tsx
@@ -0,0 +1,39 @@
+import Layout from '@/features/layout';
+import { Flex, InnerContainer, Image, Typo, Button, Icon } from '@/components/common';
+import CompanyLogo from '@assets/images/coupang.png';
+import CompanyInfo from '@/features/companies/CompanyInfo';
+import { company, recruitmentList } from '@/pages/myCompany/index.mock';
+import { palettes } from '@/assets/styles/global/palettes';
+import { buttonStyle, imageStyle, companyWrapperStyle, innerContainerStyle } from './index.styles';
+import RecruitmentList from '@/features/recruitments/RecruitmentList';
+
+export default function MyCompany() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ 회사 정보 수정하기
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/resume/Resume.tsx b/src/pages/resume/Resume.tsx
new file mode 100644
index 0000000..5d165f9
--- /dev/null
+++ b/src/pages/resume/Resume.tsx
@@ -0,0 +1,164 @@
+import Layout from '@/features/layout';
+import { Flex, Typo, Modal, Button } from '@/components/common';
+import styled from '@emotion/styled';
+import { SelectKoreanLevel, InputResumeInfo, TextareaResumeInfo } from './ResumeInput';
+import { useForm, SubmitHandler } from 'react-hook-form';
+import { type ResumeInfo } from './ResumeType';
+import useToggle from '@/hooks/useToggle';
+import { useState } from 'react';
+
+export default function Resume() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm();
+ const [isToggle, toggle] = useToggle();
+ const [formData, setFormData] = useState(null);
+
+ const onSubmit: SubmitHandler = (data) => {
+ setFormData(data);
+ toggle();
+ };
+
+ const handleResumeSubmit = () => {
+ if (formData) {
+ console.log(formData);
+ alert('제출 완료!');
+ toggle();
+ }
+ };
+ return (
+
+
+
+
+ 이력서 작성
+
+
+
+ {/* 인풋 */}
+
+
+
+
+
+
+
+
+ {/* 라디오 */}
+
+
+
+ 한국어 실력
+
+
+
+
+ {errors.koreanLanguageLevel && (
+ 한국어 실력을 선택해주세요!
+ )}
+
+
+
+ 제출하기
+
+
+ {isToggle && (
+ 정말 제출하시겠습니까?
}
+ buttonChildren={제출하기}
+ onClose={toggle}
+ />
+ )}
+
+
+ );
+}
+
+const ResumeCard = styled.div`
+ width: 60%;
+ box-shadow: 0px 12px 32px 0px rgba(24, 25, 28, 0.08);
+ border-radius: 12px;
+ border: 1px solid #e9e9e9;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 60px;
+ padding: 40px 0;
+ margin: 40px 0;
+ @media (max-width: 768px) {
+ width: 80%;
+ }
+ @media (max-width: 480px) {
+ width: 95%;
+ }
+`;
+const CustomForm = styled.form`
+ width: 90%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 30px;
+`;
+const CustomBtn = styled(Button)`
+ background: #0a65cc;
+ color: white;
+ border: 1px solid #e4e5e8;
+ &:hover {
+ border: '2px solid #0A65CC';
+ background: white;
+ color: #0a65cc;
+ }
+`;
diff --git a/src/pages/resume/ResumeInput.tsx b/src/pages/resume/ResumeInput.tsx
new file mode 100644
index 0000000..5db7ce3
--- /dev/null
+++ b/src/pages/resume/ResumeInput.tsx
@@ -0,0 +1,111 @@
+import { Flex } from '@/components/common';
+import styled from '@emotion/styled';
+import { UseFormRegister, FieldErrors } from 'react-hook-form';
+import { type ResumeInfo } from './ResumeType';
+
+interface InputType {
+ id: keyof ResumeInfo;
+ label?: string;
+ register: UseFormRegister;
+ errors: FieldErrors;
+ pattern?: RegExp;
+ patternMessage?: string;
+ placeholder: string;
+}
+
+export function SelectKoreanLevel({ level, register }: { level: string; register: UseFormRegister }) {
+ return (
+
+
+
+
+ );
+}
+
+export function InputResumeInfo({ id, label, register, pattern, errors, patternMessage, placeholder }: InputType) {
+ return (
+
+
+
+
+
+ {errors[id]?.type === 'required' && {label}을(를) 입력해주세요!}
+ {errors[id]?.type === 'pattern' && {patternMessage}}
+
+ );
+}
+
+export function TextareaResumeInfo({ id, register, errors, placeholder }: InputType) {
+ return (
+
+
+
+
+
+ {errors[id]?.type === 'required' && 자기소개를 입력해주세요!}
+
+ );
+}
+
+const RadioButton = styled.input`
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 3px;
+ position: relative;
+ cursor: pointer;
+
+ &:before {
+ content: '';
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ background-color: white;
+ top: 1px;
+ left: 1px;
+ border-radius: 3px;
+ border: 1px solid #e4e5e8;
+ }
+
+ &:checked:before {
+ background-color: #0a65cc;
+ border: none;
+ }
+
+ &:checked:after {
+ content: '✔';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-55%, -55%);
+ font-size: 14px;
+ color: #fff;
+ }
+`;
+const CustomInput = styled.input`
+ display: flex;
+ flex: 1;
+ margin: 0 10px;
+ border-radius: 5px;
+ padding: 14px;
+ font-size: 16px;
+ border: 1px solid #e4e5e8;
+`;
+
+const CustomTextarea = styled.textarea`
+ width: 100%;
+ border-radius: 5px;
+ padding: 10px;
+ fontsize: 16px;
+ height: 100px;
+ resize: none;
+ border: 1px solid #e4e5e8;
+`;
+
+const Warring = styled.p`
+ color: red;
+`;
diff --git a/src/pages/resume/ResumeType.ts b/src/pages/resume/ResumeType.ts
new file mode 100644
index 0000000..d64e0b5
--- /dev/null
+++ b/src/pages/resume/ResumeType.ts
@@ -0,0 +1,8 @@
+export interface ResumeInfo {
+ applicantName: string; // 이름
+ address: string; // 주소
+ phoneNumber: string; // 번호
+ career: string; // 경력
+ koreanLanguageLevel: '초급' | '중급' | '고급'; // 한국어 실력
+ introduction: string; // 소개
+}
diff --git a/src/pages/employee/visaRegistration/index.stories.tsx b/src/pages/visaRegistration/index.stories.tsx
similarity index 100%
rename from src/pages/employee/visaRegistration/index.stories.tsx
rename to src/pages/visaRegistration/index.stories.tsx
diff --git a/src/pages/visaRegistration/index.styles.ts b/src/pages/visaRegistration/index.styles.ts
new file mode 100644
index 0000000..318aed3
--- /dev/null
+++ b/src/pages/visaRegistration/index.styles.ts
@@ -0,0 +1,37 @@
+import { palettes } from '@/assets/styles/global/palettes';
+import { responsiveStyle } from '@/utils/responsive';
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+
+export const innerContainerStyle = css`
+ ${responsiveStyle({
+ default: {
+ border: `1px solid ${palettes.borderGray}`,
+ padding: '80px 0',
+ borderRadius: '12px',
+ marginTop: '60px',
+ marginBottom: '80px',
+ },
+ tablet: {
+ padding: '80px 0',
+ },
+ mobile: {
+ padding: '60px 0',
+ },
+ })}
+`;
+
+export const titleStyle = {
+ fontWeight: 'bold',
+};
+
+export const BreakableText = styled.span`
+ ${responsiveStyle({
+ default: {
+ display: 'inline',
+ },
+ mobile: {
+ display: 'block',
+ },
+ })}
+`;
diff --git a/src/pages/visaRegistration/index.tsx b/src/pages/visaRegistration/index.tsx
new file mode 100644
index 0000000..91967e4
--- /dev/null
+++ b/src/pages/visaRegistration/index.tsx
@@ -0,0 +1,27 @@
+import { Flex, InnerContainer, Typo } from '@/components/common';
+import Layout from '@/features/layout';
+import VisaRegistrationForm from '@/features/visaRegistration';
+import { BreakableText, innerContainerStyle, titleStyle } from './index.styles';
+
+export default function VisaRegistration() {
+ return (
+
+
+
+
+
+ 외국인 번호 및 비자 발급 일자 등록
+
+
+
+
+
+
+ );
+}
diff --git a/src/routes/path.ts b/src/routes/path.ts
index 2333738..743b2c3 100644
--- a/src/routes/path.ts
+++ b/src/routes/path.ts
@@ -1,6 +1,7 @@
export const AUTH = {
SIGN_IN: '/sign-in',
SIGN_UP: '/sign-up',
+ LOADING: '/loading',
} as const;
export const APPLY = {
@@ -8,11 +9,12 @@ export const APPLY = {
APPLYPAGE: '/apply',
};
-export const EMPLOYER = {
- MY_ACCOUNT: '/employer-my-account',
-} as const;
+export const MY_ACCOUNT = {
+ EMPLOYER: '/employer-my-account',
+};
export const EMPLOYEE = {
+ EMPLOYEE_PAGE: '/employee-my-page',
VISA_REGISTRATION: '/visa-registration',
} as const;
@@ -22,8 +24,12 @@ const ROUTE_PATH = {
POST_NOTICE: '/post-notice',
APPLY,
AUTH,
- EMPLOYER,
+ APPLICANTS: '/applicants',
EMPLOYEE,
+ RESUME: '/resume',
+ MY_COMPANY: '/my-company',
+ VISA_REGISTRATION: '/visa-registration',
+ MY_ACCOUNT,
} as const;
export default ROUTE_PATH;
diff --git a/src/routes/router.tsx b/src/routes/router.tsx
index a38be75..10e3b63 100644
--- a/src/routes/router.tsx
+++ b/src/routes/router.tsx
@@ -1,14 +1,20 @@
import { createBrowserRouter } from 'react-router-dom';
import ROUTE_PATH from './path';
-import { SignIn, SignUp } from '@pages/auth';
+import SignIn from '@pages/auth/SignIn';
+import SignUp from '@pages/auth/SignUp';
+import LoadingPage from '@/pages/auth/Loading';
import App from '@/App';
import Recruit from '@/pages/recruit';
-import VisaRegistration from '@/pages/employee/visaRegistration';
+import VisaRegistration from '@/pages/visaRegistration';
import PostNotice from '@/pages/employer/postNotice/PostNotice';
import Home from '@/pages/home';
import ApplyGuide from '@/pages/apply/applyguide/ApplyGuide';
import ApplyPage from '@/pages/apply/applypage/ApplyPage';
-import EmployerMyAccount from '@/pages/employer/myAccount';
+import MyCompany from '@/pages/myCompany';
+import Applicants from '@/pages/applicants';
+import Resume from '@/pages/resume/Resume';
+import EmployerMyAccount from '@/pages/myAccount/employer';
+import EmployeeMyPage from '@/pages/employee/myPage/EmployeeMyPage';
export const router = createBrowserRouter([
{
@@ -21,14 +27,23 @@ export const router = createBrowserRouter([
path: ROUTE_PATH.AUTH.SIGN_UP,
element: ,
},
+ {
+ path: ROUTE_PATH.AUTH.LOADING,
+ element: ,
+ },
{ path: ROUTE_PATH.RECRUIT, element: },
{ path: ROUTE_PATH.POST_NOTICE, element: },
{ path: ROUTE_PATH.APPLY.GUIDE, element: },
{ path: ROUTE_PATH.APPLY.APPLYPAGE, element: },
{ path: ROUTE_PATH.RECRUIT, element: },
+ { path: ROUTE_PATH.VISA_REGISTRATION, element: },
{ path: ROUTE_PATH.EMPLOYEE.VISA_REGISTRATION, element: },
+ { path: ROUTE_PATH.EMPLOYEE.EMPLOYEE_PAGE, element: },
{ path: ROUTE_PATH.POST_NOTICE, element: },
- { path: ROUTE_PATH.EMPLOYER.MY_ACCOUNT, element: },
+ { path: ROUTE_PATH.MY_COMPANY, element: },
+ { path: ROUTE_PATH.APPLICANTS, element: },
+ { path: ROUTE_PATH.RESUME, element: },
+ { path: ROUTE_PATH.MY_ACCOUNT.EMPLOYER, element: },
],
},
]);
diff --git a/src/store/index.ts b/src/store/index.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/store/selectStore.ts b/src/store/selectStore.ts
new file mode 100644
index 0000000..afd42d6
--- /dev/null
+++ b/src/store/selectStore.ts
@@ -0,0 +1,22 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import { SelectOptionType } from '@/types/select';
+
+type SelectStore = {
+ selectedOption: SelectOptionType | null;
+ setSelectedOption: (option: SelectOptionType) => void;
+ initializeOption: (option: SelectOptionType) => void;
+};
+
+export const useSelectStore = create(
+ persist(
+ (set) => ({
+ selectedOption: null,
+ setSelectedOption: (option) => set({ selectedOption: option }),
+ initializeOption: (option) => set({ selectedOption: option }),
+ }),
+ {
+ name: 'language',
+ },
+ ),
+);
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index af7a4dd..d5f2b41 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -1,6 +1,40 @@
-export type BannerItem = {
+export type CompanyData = {
id: number;
- imageUrl;
+ name: string;
+ industryOccupation: string;
+ brand: string;
+ revenuePerYear: number;
+ logoImage: string;
+};
+
+export type ApplicantData = {
+ userId: number;
+ name: string;
+ resumeId: number;
+ applyId: number;
+ applicantNation: string;
+ korean: string;
+};
+
+export type ForeignerData = {
+ foreignerIdNumber: string;
+ visaGenerateDate: string;
+ visaExpiryDate: string;
+};
+
+export type UserData = {
+ type: string;
+ profileImage: string;
+};
+
+export type StateProps = '근로계약서 서명하기' | '채용 마감' | '지원서 검토중' | '채용 완료';
+
+export type MyRecruitListProps = {
+ id: number;
+ title: string;
+ area: string;
+ image: string;
+ state: StateProps;
};
export type RecruitmentItem = {
diff --git a/src/types/select.ts b/src/types/select.ts
new file mode 100644
index 0000000..c20568a
--- /dev/null
+++ b/src/types/select.ts
@@ -0,0 +1,5 @@
+export type SelectOptionType = {
+ value: string;
+ text: string;
+ action: () => void;
+};