From 51138940093f31828bd81b2e19cae475d9dbbcc6 Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Thu, 30 Oct 2025 22:03:48 +0900 Subject: [PATCH 01/14] add: automatic distribution file using aws system --- .github/workflows/cd_prac.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/cd_prac.yml diff --git a/.github/workflows/cd_prac.yml b/.github/workflows/cd_prac.yml new file mode 100644 index 0000000..9f28c1e --- /dev/null +++ b/.github/workflows/cd_prac.yml @@ -0,0 +1,34 @@ +name: deploy-client + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20.11.1' + + - name: Build & Export + run: | + yarn install + yarn build + + - name: Deploy to S3 and Invalidate Cloudfront in prod mode + env: + AWS_ACCESS_KEY_ID: AKIAUOI25NIUYLDVT6XX + AWS_SECRET_ACCESS_KEY: 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 + AWS_REGION: ap-northeast-2 + run: | + aws s3 sync ./dist s3://{s3 이름} --delete + aws cloudfront create-invalidation --distribution-id {Cloudfront 이름} --paths "/*"\ From f2844e37e77a823504491ca2d7b4160632455529 Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Thu, 30 Oct 2025 22:05:18 +0900 Subject: [PATCH 02/14] add: automatic distribution file using aws system --- .github/workflows/cd_prac.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd_prac.yml b/.github/workflows/cd_prac.yml index 9f28c1e..8ace293 100644 --- a/.github/workflows/cd_prac.yml +++ b/.github/workflows/cd_prac.yml @@ -26,8 +26,8 @@ jobs: - name: Deploy to S3 and Invalidate Cloudfront in prod mode env: - AWS_ACCESS_KEY_ID: AKIAUOI25NIUYLDVT6XX - AWS_SECRET_ACCESS_KEY: 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 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ap-northeast-2 run: | aws s3 sync ./dist s3://{s3 이름} --delete From ace9ac5a8e8a2554c313c94a3d9ed3975d547a2a Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Sat, 1 Nov 2025 15:16:15 +0900 Subject: [PATCH 03/14] add: automatic cd system --- .github/workflows/cd_prac.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/cd_prac.yml diff --git a/.github/workflows/cd_prac.yml b/.github/workflows/cd_prac.yml new file mode 100644 index 0000000..16bbd0d --- /dev/null +++ b/.github/workflows/cd_prac.yml @@ -0,0 +1,34 @@ +name: deploy-client + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20.11.1' + + - name: Build & Export + run: | + yarn install + yarn build + + - name: Deploy to S3 and Invalidate Cloudfront in prod mode + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ap-northeast-2 + run: | + aws s3 sync ./dist s3://hsy-amzn-bucket-demo --delete + aws cloudfront create-invalidation --distribution-id hsy-cloudfront-demo --paths "/*"\ From 81b514955e11a07fb3428a2e4ea7ef2c289427ed Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Sat, 1 Nov 2025 21:21:21 +0900 Subject: [PATCH 04/14] add filter toggle --- src/Home.module.css | 21 ++++++++++++++ src/components/Home.tsx | 63 ++++++++++++++++++++++++++++++++++++++++- src/style.css | 10 +++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/Home.module.css diff --git a/src/Home.module.css b/src/Home.module.css new file mode 100644 index 0000000..5b52ddc --- /dev/null +++ b/src/Home.module.css @@ -0,0 +1,21 @@ +.accordion { + background-color: #eee; + color: #444; + cursor: pointer; + padding: 18px; + width: 100%; + text-align: left; + border: none; + outline: none; + transition: 0.4s; +} + +.active, .accordion:hover { + background-color: #ccc; +} + +.panel { + padding: 0 18px; + background-color: white; + overflow: hidden; +} \ No newline at end of file diff --git a/src/components/Home.tsx b/src/components/Home.tsx index d181c92..79829b7 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,8 +1,69 @@ // import React from "react"; +import { useAuth } from "../contexts/AuthContext" +import { useState } from 'react'; +import styles from "../Home.module.css" const Home = () => { + const [panelExpanded, setPanelExpanded] = useState(false); + + const expandToggle = () => { + setPanelExpanded(p => !p) + } + + return ( -
{/*

스누인턴 홈페이지 입니다

*/}
+
+ + {panelExpanded && ( +
+
+

개발

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+

기획

+ + +
+
+

디자인

+ + +
+
+

마케팅

+ + +
+
+ )} + + +
+
); }; diff --git a/src/style.css b/src/style.css index 43cddc9..03ba4ef 100644 --- a/src/style.css +++ b/src/style.css @@ -20,6 +20,16 @@ body { min-height: 100vh; } +.col { + display: flex; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; +} + main { flex: 1; display: flex; From 37cb3448178539b3a3f6df50c12a88e2853a3384 Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Sun, 2 Nov 2025 13:56:10 +0900 Subject: [PATCH 05/14] feat: filter modals, query state depending on filter, new context for post data --- package-lock.json | 40 +-- package.json | 1 + src/App.tsx | 19 +- src/Home.module.css | 148 ++++++++++- src/components/Home.tsx | 482 +++++++++++++++++++++++++++++++---- src/contexts/PostContext.tsx | 135 ++++++++++ src/style.css | 2 + yarn.lock | 162 ++++++------ 8 files changed, 808 insertions(+), 181 deletions(-) create mode 100644 src/contexts/PostContext.tsx diff --git a/package-lock.json b/package-lock.json index 2e2efbf..7c30293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,10 @@ "name": "wafflestudio-2025-fe-seminar-template", "version": "0.0.0", "dependencies": { - "@types/react-router-dom": "^5.3.3", "axios": "^1.12.2", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.9.4" }, "devDependencies": { @@ -1544,12 +1544,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.18.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.9.tgz", @@ -1564,6 +1558,7 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1579,27 +1574,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", @@ -1689,6 +1663,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -2368,6 +2343,15 @@ "react": "^19.2.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-router": { "version": "7.9.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", diff --git a/package.json b/package.json index 1294708..d7a7082 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axios": "^1.12.2", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.9.4" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index 265231e..ec393b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,18 +4,21 @@ import Login from './components/Login'; import Signup from './components/Signup'; import Topbar from './components/Topbar'; import { AuthProvider } from './contexts/AuthContext'; +import { PostProvider } from './contexts/postContext'; const App = () => ( - -
- - } /> - } /> - } /> - -
+ + +
+ + } /> + } /> + } /> + +
+
); diff --git a/src/Home.module.css b/src/Home.module.css index 5b52ddc..5f96a0a 100644 --- a/src/Home.module.css +++ b/src/Home.module.css @@ -1,15 +1,27 @@ +.main{ + display: flex; + flex-direction: column; + padding: 16px 30px; +} + .accordion { background-color: #eee; color: #444; cursor: pointer; - padding: 18px; + padding: 16px 24px 16px 24px; width: 100%; - text-align: left; + display: flex; + flex-direction: row; border: none; outline: none; transition: 0.4s; + justify-content: space-between; + + font-size: 18px; + font-weight: 700 } + .active, .accordion:hover { background-color: #ccc; } @@ -18,4 +30,134 @@ padding: 0 18px; background-color: white; overflow: hidden; -} \ No newline at end of file +} + +ul.chips { + margin: 30px 0 50px; + list-style: none; + display: flex; + gap: 6px; + flex-wrap: wrap; + padding: 0px 16px 16px 16px; +} + +.chip { + background-color: rgb(255, 255, 255); + display: flex; + align-items: center; + gap: 4px; + margin-right: 6px; + font-size: 12px; + float:left; + border: 1px solid #F1F2F5; + border-radius: 14px; + padding:16px 8px; + font-size: 14px; + } + + .resetChip { + background-color: rgb(255, 255, 255); + display: flex; + align-items: center; + gap: 4px; + margin-right: 6px; + font-size: 12px; + float:left; + padding:16px 8px; + font-size: 14px; + border: none; + } + + /* MODALS */ + .modalTrigger { + position: relative; + } + .modal { + position: absolute; + top: calc(100% + 8px); + left: 0; + background-color: white; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + padding:16px; + width: 250px; + z-index: 1000; + cursor: default; + } + + .modalOptions { + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: 16px; + } +.radioLabel { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; +} +.modalOptions label { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +/* If your JSX uses a plain `className="row"`, define that globally in `src/style.css`. + Prefer using `styles.row` (CSS modules) instead; see `Home.tsx` for where .row is used. */ + .modalActions { + display: flex; + justify-content: flex-end; + gap: 8px; + border-top: 1px solid #f0f0f0; + padding-top: 16px; + } +.btn { + border: none; + border-radius: 6px; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + cursor: pointer; +} +.btnReset { + background-color: #f1f2f5; + color: #555; +} +.btnApply { + background-color: #333; + color: white; +} + +.radioLabel input[type="radio"] { + display: none; +} +.radioVisual { + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #ccc; + display: grid; + place-items: center; + transition: all 0.2s; +} +.radioVisual::before { /* inner dot */ + content: ""; + width: 10px; + height: 10px; + border-radius: 50px; + background-color: #555; + transform: scale(0); + transition: all 0.2s; +} +.radioLabel input[type="radio"]:checked + .radioVisual { + border-color: #555; +} +.radioLabel input[type="radio"]:checked + .radioVisual::before { + transform: scale(1); +} + +/* input[type="checkbox"] { + +} */ \ No newline at end of file diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 79829b7..4a4112b 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,69 +1,443 @@ +import { type ChangeEvent, useEffect, useState } from 'react'; +import { FaAngleDown, FaAngleUp, FaArrowRotateRight } from 'react-icons/fa6'; +import styles from '../Home.module.css'; // import React from "react"; -import { useAuth } from "../contexts/AuthContext" -import { useState } from 'react'; -import styles from "../Home.module.css" +// import { useAuth } from '../contexts/AuthContext'; +import { encodeQueryParams, usePosts } from '../contexts/postContext'; const Home = () => { + const DOMAINS: string[] = [ + 'FINTECH', + 'HEALTHTECH', + 'EDUCATION', + 'ECOMMERCE', + 'FOODTECH', + 'MOBILITY', + 'CONTENTS', + 'B2B', + 'OTHERS', + ]; + + const DEV: string[] = ['FRONT', 'APP', 'BACKEND', 'DATA', 'OTHERS']; + + const { fetchPosts } = usePosts(); const [panelExpanded, setPanelExpanded] = useState(false); + const [roles, setRoles] = useState([]); + + const [recExpanded, setRecExpanded] = useState(false); + const [isActive, setIsActive] = useState(false); + // radio selection state (temporary) + const [recStatus, setRecStatus] = useState<'all' | 'recruiting'>( + isActive ? 'recruiting' : 'all' + ); + + const [domains, setDomain] = useState(DOMAINS); + const [dmStatus, setDmStatus] = useState(domains); + const [dmExpanded, setDmExpanded] = useState(false); + + const [order, setOrder] = useState(0); + const [newExpanded, setNewExpanded] = useState(false); + const [orderStatus, setOrderStatus] = useState(0); - const expandToggle = () => { - setPanelExpanded(p => !p) - } + const [query, setQuery] = useState(''); + // make fetch api call + useEffect(() => { + fetchPosts(query); + }, [query, fetchPosts]); + + // when filter settings change, re-calculate query (leads to above useEffect, i.e fetchPosts) + useEffect(() => { + const q: string = encodeQueryParams({ + params: { + roles: `${roles.length === 0 ? null : roles}`, + isActive: `${isActive}`, + domains: `${domains.length === 0 ? null : domains}`, + order: `${order}`, + }, + }); + setQuery(q); + }, [roles, isActive, domains, order]); + // role check + const handleRoleCheck = (e: ChangeEvent) => { + const { checked, value } = e.currentTarget; + if (value === 'total') { + setRoles((p) => [...DEV, ...p.filter((v) => !DEV.includes(v))]); + } else { + setRoles((p) => { + if (checked) return [...p, value]; + return p.filter((v) => v !== value); + }); + } + }; + // domain check + const handleDomainCheck = (e: ChangeEvent) => { + const { checked, value } = e.currentTarget; + if (value === 'all') { + setDmStatus(DOMAINS); + } else { + setDmStatus((p) => { + if (checked) return [...p, value]; + return p.filter((v) => v !== value); + }); + } + }; + + const handleReset = () => { + // reset 모집 상태 selection and other filters + setIsActive(false); + setRoles([]); + setDomain(DOMAINS); + setOrder(0); + }; return ( -
- +
+ {panelExpanded && ( -
-
-

개발

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - - -
-
-

기획

- - -
-
-

디자인

- - -
-
-

마케팅

- - -
-
+
+
+

개발

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

기획

+ + +
+ +
+

디자인

+ + +
+
+

마케팅

+ + +
+
)} +
    + {/* 모집 상태 옵션 버튼 */} +
  • { + setRecExpanded((p) => !p); + }} + > + 모집 상태 {recExpanded ? : } + {/* 모집 상태 팝업 */} + {recExpanded && ( +
    e.stopPropagation()} // prevents clicks inside from closing modal + > +
    + + + +
    +
    + + +
    +
    + )} +
  • + + {/* 업종 옵션 버튼 */} +
  • { + setDmExpanded((p) => !p); + }} + > + 업종 {dmExpanded ? : } + {/* 업종 선택 팝업 */} + {dmExpanded && ( +
    e.stopPropagation()}> +
    + + dmStatus.includes(d)) + } + onChange={handleDomainCheck} + /> + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + )} +
  • -
    -
+ {/* 순서 */} +
  • { + setNewExpanded((p) => !p); + }} + > + 최신순 {newExpanded ? : } + {/* 순서 선택 팝업 */} + {newExpanded && ( +
    e.stopPropagation()}> +
    + + +
    +
    + + +
    +
    + )} +
  • +
  • + 초기화 +
  • + +
    ); }; diff --git a/src/contexts/PostContext.tsx b/src/contexts/PostContext.tsx new file mode 100644 index 0000000..67a878e --- /dev/null +++ b/src/contexts/PostContext.tsx @@ -0,0 +1,135 @@ +import { type ReactNode, createContext, useContext, useState } from 'react'; +import apiClient from '../api'; + +interface Post { + companyName: string; + profileImageKey: string; + employmentEndDate: string; + positionTitle: string; + domain: string; + detailSummary: string; + positionType: string; +} + +interface ApiAuthor { + id: string; + name: string; + profileImageKey: string; +} + +interface ApiTag { + tag: string; +} + +interface ApiPost { + id: string; + author: ApiAuthor; + companyName: string; + profileImageKey: string; + location: string; + employmentEndDate: string; // ISO date string + positionTitle: string; + domain: string; + slogan: string; + detailSummary: string; + positionType: string; + headCount: number; + isBookmarked: boolean; + createdAt: string; // This is an ISO date string + updatedAt: string; // This is an ISO date string + tags: ApiTag[]; + coffeeChatCount: number; +} + +interface PostContextType { + posts: Post[] | null; + paginator: number; + isLoading: boolean; + fetchPosts: (query: string) => Promise; +} + +export const encodeQueryParams = ({ + params, +}: { + params: Record< + string, + | string + | number + | boolean + | string[] + | number[] + | boolean[] + | null + | undefined + >; +}) => { + const queryParameters = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === null) return; // null, undefined 제외 + + if (Array.isArray(value)) { + value.forEach((v) => { + queryParameters.append(key, v.toString()); + }); + } else { + queryParameters.append(key, value.toString()); + } + }); + + return queryParameters.toString(); +}; + +const PostContext = createContext(undefined); + +interface PostProviderProps { + children: ReactNode; +} + +export const PostProvider = ({ children }: PostProviderProps) => { + const [posts, setPosts] = useState(null); + const [paginator, setPaginator] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + const fetchPosts = async (query: string) => { + setIsLoading(true); + try { + const url = `/api/post?${query}`; + const response = await apiClient.get<{ + posts: ApiPost[]; + paginator: { lastpage: number }; + }>(url); + setPaginator(response.data.paginator.lastpage); + const apiPosts = response.data.posts; + const formattedPosts: Post[] = apiPosts.map((p: ApiPost) => ({ + companyName: p.companyName, + profileImageKey: p.profileImageKey, + employmentEndDate: p.employmentEndDate, + positionTitle: p.positionTitle, + domain: p.domain, + detailSummary: p.detailSummary, + positionType: p.positionType, + })); + setPosts(formattedPosts); + } catch (error) { + console.error('Failed to fetch posts:', error); + setPosts(null); + } finally { + setIsLoading(false); + } + }; + + return ( + + {children} + + ); +}; + +export const usePosts = () => { + const context = useContext(PostContext); + if (context === undefined) { + throw new Error('usePosts must be used within a PostProvider'); + } + return context; +}; diff --git a/src/style.css b/src/style.css index 03ba4ef..1f12d1b 100644 --- a/src/style.css +++ b/src/style.css @@ -28,6 +28,8 @@ body { .row { display: flex; flex-direction: row; + align-items: center; + gap: 8px; } main { diff --git a/yarn.lock b/yarn.lock index 28902c6..f085877 100644 --- a/yarn.lock +++ b/yarn.lock @@ -97,21 +97,21 @@ __metadata: linkType: hard "@emnapi/core@npm:^1.5.0": - version: 1.5.0 - resolution: "@emnapi/core@npm:1.5.0" + version: 1.6.0 + resolution: "@emnapi/core@npm:1.6.0" dependencies: "@emnapi/wasi-threads": "npm:1.1.0" tslib: "npm:^2.4.0" - checksum: 10c0/52ba3485277706d92fa27d92b37e5b4f6ef0742c03ed68f8096f294c6bfa30f0752c82d4c2bfa14bff4dc30d63c9f71a8f9fb64a92743d00807d9e468fafd5ff + checksum: 10c0/40e384f39104a9f8260e671c0110f8618961afc564afb2e626af79175717a8b5e2d8b2ae3d30194d318a71247e0fc833601666233adfeb244c46cadc06c58a51 languageName: node linkType: hard "@emnapi/runtime@npm:^1.5.0": - version: 1.5.0 - resolution: "@emnapi/runtime@npm:1.5.0" + version: 1.6.0 + resolution: "@emnapi/runtime@npm:1.6.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/a85c9fc4e3af49cbe41e5437e5be2551392a931910cd0a5b5d3572532786927810c9cc1db11b232ec8f9657b33d4e6f7c4f985f1a052917d7cd703b5b2a20faa + checksum: 10c0/e3d2452a8fb83bb59fe60dfcf4cff99f9680c13c07dff8ad28639ccc8790151841ef626a67014bde132939bad73dfacc440ade8c3db2ab12693ea9c8ba4d37fb languageName: node linkType: hard @@ -692,92 +692,92 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-darwin-arm64@npm:1.13.20" +"@swc/core-darwin-arm64@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-darwin-arm64@npm:1.13.5" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-darwin-x64@npm:1.13.20" +"@swc/core-darwin-x64@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-darwin-x64@npm:1.13.5" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.20" +"@swc/core-linux-arm-gnueabihf@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.5" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-arm64-gnu@npm:1.13.20" +"@swc/core-linux-arm64-gnu@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-arm64-gnu@npm:1.13.5" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-arm64-musl@npm:1.13.20" +"@swc/core-linux-arm64-musl@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-arm64-musl@npm:1.13.5" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-x64-gnu@npm:1.13.20" +"@swc/core-linux-x64-gnu@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-x64-gnu@npm:1.13.5" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-x64-musl@npm:1.13.20" +"@swc/core-linux-x64-musl@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-x64-musl@npm:1.13.5" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-win32-arm64-msvc@npm:1.13.20" +"@swc/core-win32-arm64-msvc@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-win32-arm64-msvc@npm:1.13.5" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-win32-ia32-msvc@npm:1.13.20" +"@swc/core-win32-ia32-msvc@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-win32-ia32-msvc@npm:1.13.5" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-win32-x64-msvc@npm:1.13.20" +"@swc/core-win32-x64-msvc@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-win32-x64-msvc@npm:1.13.5" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.12.11": - version: 1.13.20 - resolution: "@swc/core@npm:1.13.20" - dependencies: - "@swc/core-darwin-arm64": "npm:1.13.20" - "@swc/core-darwin-x64": "npm:1.13.20" - "@swc/core-linux-arm-gnueabihf": "npm:1.13.20" - "@swc/core-linux-arm64-gnu": "npm:1.13.20" - "@swc/core-linux-arm64-musl": "npm:1.13.20" - "@swc/core-linux-x64-gnu": "npm:1.13.20" - "@swc/core-linux-x64-musl": "npm:1.13.20" - "@swc/core-win32-arm64-msvc": "npm:1.13.20" - "@swc/core-win32-ia32-msvc": "npm:1.13.20" - "@swc/core-win32-x64-msvc": "npm:1.13.20" + version: 1.13.5 + resolution: "@swc/core@npm:1.13.5" + dependencies: + "@swc/core-darwin-arm64": "npm:1.13.5" + "@swc/core-darwin-x64": "npm:1.13.5" + "@swc/core-linux-arm-gnueabihf": "npm:1.13.5" + "@swc/core-linux-arm64-gnu": "npm:1.13.5" + "@swc/core-linux-arm64-musl": "npm:1.13.5" + "@swc/core-linux-x64-gnu": "npm:1.13.5" + "@swc/core-linux-x64-musl": "npm:1.13.5" + "@swc/core-win32-arm64-msvc": "npm:1.13.5" + "@swc/core-win32-ia32-msvc": "npm:1.13.5" + "@swc/core-win32-x64-msvc": "npm:1.13.5" "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.25" + "@swc/types": "npm:^0.1.24" peerDependencies: "@swc/helpers": ">=0.5.17" dependenciesMeta: @@ -804,7 +804,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10c0/ef9ed965f20fe8331ef6b279b30ae2421ec9cd4105cfc0ac4645ae716844dfa9998162b132abbd6e8ccfb91deb263861792cf815fdcdcb7a011384e3505e0532 + checksum: 10c0/26efc58d2b154050a4d75b215007e2780fc02ccdd35168746e818ef58009201878d5fbe0e074ed2902858c447fd8806e52c03dd05c20ba8501573f57ef34c3be languageName: node linkType: hard @@ -815,7 +815,7 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:^0.1.25": +"@swc/types@npm:^0.1.24": version: 0.1.25 resolution: "@swc/types@npm:0.1.25" dependencies: @@ -841,11 +841,11 @@ __metadata: linkType: hard "@types/node@npm:^22.15.29": - version: 22.18.10 - resolution: "@types/node@npm:22.18.10" + version: 22.18.9 + resolution: "@types/node@npm:22.18.9" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/37b34553f1953ab8fc25b7467d44701446400f62547adb62c842cebac091350985470aeed87acd60f5295778c370d9721d477310e643677a7eab03a8dc6ed74f + checksum: 10c0/0d342e17e0b36a4475ba36f0a0b9d7e28cf85ae44b1ea1f218c8cffe8c9519820eef9b97c171b0b93d8066c8710d7660e31660678bac0842655dff2118a23248 languageName: node linkType: hard @@ -930,20 +930,6 @@ __metadata: languageName: node linkType: hard -"async-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-function@npm:1.0.0" - checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 - languageName: node - linkType: hard - -"async-generator-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-generator-function@npm:1.0.0" - checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 - languageName: node - linkType: hard - "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -1410,31 +1396,21 @@ __metadata: languageName: node linkType: hard -"generator-function@npm:^2.0.0": - version: 2.0.1 - resolution: "generator-function@npm:2.0.1" - checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 - languageName: node - linkType: hard - "get-intrinsic@npm:^1.2.6": - version: 1.3.1 - resolution: "get-intrinsic@npm:1.3.1" + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" dependencies: - async-function: "npm:^1.0.0" - async-generator-function: "npm:^1.0.0" call-bind-apply-helpers: "npm:^1.0.2" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" - generator-function: "npm:^2.0.0" get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-symbols: "npm:^1.1.0" hasown: "npm:^2.0.2" math-intrinsics: "npm:^1.1.0" - checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d + checksum: 10c0/52c81808af9a8130f581e6a6a83e1ba4a9f703359e7a438d1369a5267a25412322f03dcbd7c549edaef0b6214a0630a28511d7df0130c93cfd380f4fa0b5b66a languageName: node linkType: hard @@ -1847,8 +1823,8 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 11.4.2 - resolution: "node-gyp@npm:11.4.2" + version: 11.5.0 + resolution: "node-gyp@npm:11.5.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -1862,7 +1838,7 @@ __metadata: which: "npm:^5.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10c0/0bfd3e96770ed70f07798d881dd37b4267708966d868a0e585986baac487d9cf5831285579fd629a83dc4e434f53e6416ce301097f2ee464cb74d377e4d8bdbe + checksum: 10c0/31ff49586991b38287bb15c3d529dd689cfc32f992eed9e6997b9d712d5d21fe818a8b1bbfe3b76a7e33765c20210c5713212f4aa329306a615b87d8a786da3a languageName: node linkType: hard @@ -2048,6 +2024,15 @@ __metadata: languageName: node linkType: hard +"react-icons@npm:^5.5.0": + version: 5.5.0 + resolution: "react-icons@npm:5.5.0" + peerDependencies: + react: "*" + checksum: 10c0/a24309bfc993c19cbcbfc928157e53a137851822779977b9588f6dd41ffc4d11ebc98b447f4039b0d309a858f0a42980f6bfb4477fb19f9f2d1bc2e190fcf79c + languageName: node + linkType: hard + "react-router-dom@npm:^7.9.4": version: 7.9.4 resolution: "react-router-dom@npm:7.9.4" @@ -2339,15 +2324,15 @@ __metadata: linkType: hard "tar@npm:^7.4.3": - version: 7.5.1 - resolution: "tar@npm:7.5.1" + version: 7.5.2 + resolution: "tar@npm:7.5.2" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/0dad0596a61586180981133b20c32cfd93c5863c5b7140d646714e6ea8ec84583b879e5dc3928a4d683be6e6109ad7ea3de1cf71986d5194f81b3a016c8858c9 + checksum: 10c0/a7d8b801139b52f93a7e34830db0de54c5aa45487c7cb551f6f3d44a112c67f1cb8ffdae856b05fd4f17b1749911f1c26f1e3a23bbe0279e17fd96077f13f467 languageName: node linkType: hard @@ -2490,6 +2475,7 @@ __metadata: knip: "npm:^5.59.1" react: "npm:^19.1.0" react-dom: "npm:^19.1.0" + react-icons: "npm:^5.5.0" react-router-dom: "npm:^7.9.4" typescript: "npm:~5.8.3" vite: "npm:^6.3.5" From 5b83a41314508fe222c8b54843fc7202147916b3 Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Sun, 2 Nov 2025 15:14:14 +0900 Subject: [PATCH 06/14] fix: api call --- src/components/Home.tsx | 283 +++++++++++++++++++---------------- src/contexts/PostContext.tsx | 27 +++- 2 files changed, 175 insertions(+), 135 deletions(-) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 4a4112b..71d854c 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,7 +1,6 @@ import { type ChangeEvent, useEffect, useState } from 'react'; import { FaAngleDown, FaAngleUp, FaArrowRotateRight } from 'react-icons/fa6'; import styles from '../Home.module.css'; -// import React from "react"; // import { useAuth } from '../contexts/AuthContext'; import { encodeQueryParams, usePosts } from '../contexts/postContext'; @@ -50,10 +49,10 @@ const Home = () => { useEffect(() => { const q: string = encodeQueryParams({ params: { - roles: `${roles.length === 0 ? null : roles}`, - isActive: `${isActive}`, - domains: `${domains.length === 0 ? null : domains}`, - order: `${order}`, + roles: roles.length === 0 ? null : roles, + isActive: isActive, + domains: domains.length === DOMAINS.length ? null : domains, + order: order, }, }); setQuery(q); @@ -89,6 +88,10 @@ const Home = () => { setRoles([]); setDomain(DOMAINS); setOrder(0); + + setRecStatus('all'); + setDmStatus(DOMAINS); + setOrderStatus(0); }; return ( @@ -111,6 +114,7 @@ const Home = () => {

    개발

    + { value="total" onChange={handleRoleCheck} > -
    + { value="FRONT" onChange={handleRoleCheck} > -
    + { value="BACKEND" onChange={handleRoleCheck} > -
    + { value="APP" onChange={handleRoleCheck} > -
    + { value="OTHERS" onChange={handleRoleCheck} > -

    기획

    - - +
    + + +

    디자인

    - - +
    + + +

    마케팅

    - - +
    + + +
    )} @@ -269,99 +278,119 @@ const Home = () => { {dmExpanded && (
    e.stopPropagation()}>
    - - dmStatus.includes(d)) - } - onChange={handleDomainCheck} - /> - - - - - - - - - - - - - - - - - - +
    + + dmStatus.includes(d)) + } + onChange={handleDomainCheck} + /> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    - {panelExpanded && ( -
    -
    -

    개발

    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -

    기획

    -
    - - -
    -
    - -
    -

    디자인

    -
    - - -
    -
    -
    -

    마케팅

    -
    - - +
    +
    +
    + + {panelExpanded && ( +
    +
    +

    개발

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +

    기획

    +
    + + +
    +
    + +
    +

    디자인

    +
    + + +
    +
    +
    +

    마케팅

    +
    + + +
    +
    -
    + )}
    - )} -
    - -
    -
    -
    +
      {/* 모집 상태 옵션 버튼 */}
    • {
    {renderPageNumbers()} -
    From dfe2aacef3568180d526daf73d023c2c9506a445 Mon Sep 17 00:00:00 2001 From: Jeongyeon Lee Date: Sun, 2 Nov 2025 17:34:03 +0900 Subject: [PATCH 10/14] InternCard Added (#9) --- src/Home.module.css | 61 +++++++++++++++++++++++++ src/InternCard.module.css | 85 +++++++++++++++++++++++++++++++++++ src/components/Home.tsx | 47 ++++++++++++++++++- src/components/InternCard.tsx | 82 +++++++++++++++++++++++++++++++++ src/contexts/PostContext.tsx | 65 ++++++++++++++++++++++----- src/types/types.ts | 12 +++++ 6 files changed, 340 insertions(+), 12 deletions(-) create mode 100644 src/InternCard.module.css create mode 100644 src/components/InternCard.tsx diff --git a/src/Home.module.css b/src/Home.module.css index e178778..13ac1cb 100644 --- a/src/Home.module.css +++ b/src/Home.module.css @@ -163,3 +163,64 @@ ul.chips { /* input[type="checkbox"] { } */ + +/* Login Confirmation Modal Styles */ +.modalBackdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modalContent { + background-color: white; + padding: 24px; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + text-align: center; + width: 90%; + max-width: 320px; +} + +.modalContent p { + margin: 0 0 16px; + font-size: 16px; +} + +.modalActions { + display: flex; + gap: 12px; + justify-content: center; +} + +.modalActions button { + padding: 10px 20px; + border-radius: 6px; + border: 1px solid #ccc; + background-color: #f0f0f0; + cursor: pointer; + font-size: 14px; + font-weight: 500; + color: #212529; /* Dark text color for better visibility */ + transition: background-color 0.2s; +} + +.modalActions button:first-child:hover { + background-color: #e2e6ea; /* Light grey on hover */ +} + +.modalActions button:last-child { + background-color: #007bff; + color: white; + border-color: #007bff; +} + +.modalActions button:last-child:hover { + background-color: #0069d9; +} diff --git a/src/InternCard.module.css b/src/InternCard.module.css new file mode 100644 index 0000000..8580225 --- /dev/null +++ b/src/InternCard.module.css @@ -0,0 +1,85 @@ +.card { + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: box-shadow 0.2s ease-in-out; + display: flex; + flex-direction: column; + gap: 12px; +} + +.card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.header { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.logo { + width: 48px; + height: 48px; + border-radius: 4px; + background-color: #e0e0e0; +} + +.titleGroup { + flex: 1; +} + +.companyName { + font-size: 16px; + font-weight: 600; + margin: 0; +} + +.positionTitle { + font-size: 14px; + color: #555; + margin: 4px 0 0; +} + +.bookmarkBtn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + font-size: 20px; +} + +.body { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.tag { + background-color: #f0f0f0; + color: #333; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; +} + +.footer { + display: flex; + justify-content: flex-end; + align-items: center; +} + +.dDay { + font-size: 14px; + font-weight: 500; + color: #e53e3e; +} + +.deadline { + font-size: 14px; + font-weight: 500; + color: #888; +} diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 5e3266e..d81f655 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,8 +1,10 @@ import { type ChangeEvent, useEffect, useState } from 'react'; import { FaAngleDown, FaAngleUp, FaArrowRotateRight } from 'react-icons/fa6'; +import { useNavigate } from 'react-router-dom'; import styles from '../Home.module.css'; // import { useAuth } from '../contexts/AuthContext'; import { encodeQueryParams, usePosts } from '../contexts/PostContext'; +import InternCard from './InternCard'; const Home = () => { const DOMAINS: string[] = [ @@ -19,7 +21,7 @@ const Home = () => { const DEV: string[] = ['FRONT', 'APP', 'BACKEND', 'DATA', 'OTHERS']; - const { fetchPosts } = usePosts(); + const { posts, isLoading, fetchPosts } = usePosts(); const [panelExpanded, setPanelExpanded] = useState(false); const [roles, setRoles] = useState([]); @@ -40,6 +42,13 @@ const Home = () => { const [query, setQuery] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const navigate = useNavigate(); + + const handleLoginRequired = () => { + setIsModalOpen(true); + }; + // make fetch api call useEffect(() => { fetchPosts(query); @@ -466,6 +475,42 @@ const Home = () => { 초기화 + +
    + {isLoading ? ( +

    Loading...

    + ) : ( + posts?.map((post) => ( + + )) + )} +
    + + {isModalOpen && ( +
    setIsModalOpen(false)} + > +
    e.stopPropagation()} + > +

    로그인이 필요한 기능입니다.

    +
    + + +
    +
    +
    + )}
    ); }; diff --git a/src/components/InternCard.tsx b/src/components/InternCard.tsx new file mode 100644 index 0000000..6d1bddb --- /dev/null +++ b/src/components/InternCard.tsx @@ -0,0 +1,82 @@ +import { FaBookmark, FaRegBookmark } from 'react-icons/fa6'; +import styles from '../InternCard.module.css'; +import { useAuth } from '../contexts/AuthContext'; +import { usePosts } from '../contexts/PostContext'; +import type { Post } from '../types/types'; + +interface InternCardProps { + post: Post; + onLoginRequired: () => void; +} + +const InternCard = ({ post, onLoginRequired }: InternCardProps) => { + const { user } = useAuth(); + const { toggleBookmark } = usePosts(); + + const handleBookmarkClick = async () => { + if (!user) { + onLoginRequired(); + return; + } + await toggleBookmark(post.id); + }; + + const calculateDday = (dateString: string) => { + const endDate = new Date(dateString); + const today = new Date(); + // UTC 기준으로 변환하여 계산 + const utcEndDate = Date.UTC( + endDate.getFullYear(), + endDate.getMonth(), + endDate.getDate() + ); + const utcToday = Date.UTC( + today.getFullYear(), + today.getMonth(), + today.getDate() + ); + const diffTime = utcEndDate - utcToday; + const diffDays = diffTime / (1000 * 60 * 60 * 24); + + if (diffDays < 0) return '마감'; + if (diffDays === 0) return 'D-day'; + return `D-${diffDays}`; + }; + + const dDay = calculateDday(post.employmentEndDate); + + return ( +
    +
    +
    +
    +

    {post.companyName}

    +

    {post.positionTitle}

    +
    + +
    +
    + {post.domain} + {post.positionType} +
    +
    + + {dDay} + +
    +
    + ); +}; + +export default InternCard; diff --git a/src/contexts/PostContext.tsx b/src/contexts/PostContext.tsx index 1a62dc1..d80f71e 100644 --- a/src/contexts/PostContext.tsx +++ b/src/contexts/PostContext.tsx @@ -3,20 +3,13 @@ import { createContext, useCallback, useContext, + useEffect, useMemo, useState, } from 'react'; import apiClient from '../api'; - -interface Post { - companyName: string; - profileImageKey: string; - employmentEndDate: string; - positionTitle: string; - domain: string; - detailSummary: string; - positionType: string; -} +import type { Post } from '../types/types'; +import { useAuth } from './AuthContext'; interface ApiAuthor { id: string; @@ -53,6 +46,7 @@ interface PostContextType { paginator: number; isLoading: boolean; fetchPosts: (query: string) => Promise; + toggleBookmark: (postId: string) => void; } export const encodeQueryParams = ({ @@ -97,6 +91,17 @@ export const PostProvider = ({ children }: PostProviderProps) => { const [posts, setPosts] = useState(null); const [paginator, setPaginator] = useState(0); const [isLoading, setIsLoading] = useState(false); + const { user } = useAuth(); + + useEffect(() => { + if (!user) { + // User logged out, clear all bookmarks from the current state + setPosts((prevPosts) => { + if (!prevPosts) return null; + return prevPosts.map((post) => ({ ...post, isBookmarked: false })); + }); + } + }, [user]); const fetchPosts = useCallback(async (query: string) => { setIsLoading(true); @@ -109,6 +114,7 @@ export const PostProvider = ({ children }: PostProviderProps) => { setPaginator(response.data.paginator.lastpage); const apiPosts = response.data.posts; const formattedPosts: Post[] = apiPosts.map((p: ApiPost) => ({ + id: p.id, companyName: p.companyName, profileImageKey: p.profileImageKey, employmentEndDate: p.employmentEndDate, @@ -116,6 +122,7 @@ export const PostProvider = ({ children }: PostProviderProps) => { domain: p.domain, detailSummary: p.detailSummary, positionType: p.positionType, + isBookmarked: p.isBookmarked, })); setPosts(formattedPosts); } catch (error) { @@ -125,14 +132,50 @@ export const PostProvider = ({ children }: PostProviderProps) => { setIsLoading(false); } }, []); + + const toggleBookmark = useCallback( + async (postId: string) => { + // Optimistic UI update + const originalPosts = posts; + setPosts((prevPosts) => { + if (!prevPosts) return null; + return prevPosts.map((post) => + post.id === postId + ? { ...post, isBookmarked: !post.isBookmarked } + : post + ); + }); + + const postToUpdate = originalPosts?.find((p) => p.id === postId); + if (!postToUpdate) return; + + try { + if (postToUpdate.isBookmarked) { + // 북마크 해제 API 호출 + await apiClient.delete(`/api/post/${postId}/bookmark`); + } else { + // 북마크 추가 API 호출 + await apiClient.post(`/api/post/${postId}/bookmark`); + } + } catch (error) { + console.error('북마크 처리 실패:', error); + alert('북마크 처리에 실패했습니다.'); + // API 호출 실패 시, 상태를 원래대로 되돌림 + setPosts(originalPosts); + } + }, + [posts] + ); + const value = useMemo( () => ({ posts, isLoading, paginator, fetchPosts, + toggleBookmark, }), - [posts, isLoading, paginator, fetchPosts] + [posts, isLoading, paginator, fetchPosts, toggleBookmark] ); return {children}; }; diff --git a/src/types/types.ts b/src/types/types.ts index 0729847..0f99f6b 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -8,3 +8,15 @@ export interface LoginData { email: string; password: string; } + +export interface Post { + id: string; + companyName: string; + profileImageKey: string; + employmentEndDate: string; + positionTitle: string; + domain: string; + detailSummary: string; + positionType: string; + isBookmarked: boolean; +} From 8bff5edfab5cc1988ca0bdb379b3c04aa295fe29 Mon Sep 17 00:00:00 2001 From: Jeongyeon Lee Date: Mon, 3 Nov 2025 13:36:02 +0900 Subject: [PATCH 11/14] Intern component (#10) --- src/Home.module.css | 82 +++- src/components/Home.tsx | 885 +++++++++++++++++++---------------- src/contexts/PostContext.tsx | 2 +- 3 files changed, 544 insertions(+), 425 deletions(-) diff --git a/src/Home.module.css b/src/Home.module.css index 13ac1cb..0a2c8dd 100644 --- a/src/Home.module.css +++ b/src/Home.module.css @@ -1,14 +1,29 @@ +.filterContainer { + position: sticky; + top: 0; + z-index: 100; + background-color: white; /* Ensure background is white to cover content when sticky */ + padding: 16px 30px 0px 30px; /* Add padding to match main content */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Optional: Add shadow for better visual separation */ +} + .main { display: flex; flex-direction: column; - padding: 16px 30px; + padding: 0 30px 16px 30px; /* Adjust padding for main content */ +} + +.contentWrapper { + display: flex; + flex-direction: column; + gap: 20px; } .accordion { background-color: #eee; color: #444; cursor: pointer; - padding: 16px 24px 16px 24px; + padding: 16px 24px; width: 100%; display: flex; flex-direction: row; @@ -16,7 +31,6 @@ outline: none; transition: 0.4s; justify-content: space-between; - font-size: 18px; font-weight: 700; } @@ -33,39 +47,57 @@ } ul.chips { - margin: 30px 0 50px; + margin: 0; list-style: none; display: flex; gap: 6px; flex-wrap: wrap; - padding: 0px 16px 16px 16px; + padding: 0; } -.chip { - background-color: rgb(255, 255, 255); - display: flex; - align-items: center; - gap: 4px; - margin-right: 6px; - font-size: 12px; - float: left; - border: 1px solid #f1f2f5; - border-radius: 14px; - padding: 16px 8px; - font-size: 14px; +.postList { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; /* Gap between cards */ } -.resetChip { - background-color: rgb(255, 255, 255); +.pagination { display: flex; + justify-content: center; align-items: center; - gap: 4px; - margin-right: 6px; - font-size: 12px; - float: left; - padding: 16px 8px; + gap: 5px; /* Smaller gap between page buttons */ + margin-top: 20px; +} + +.pagination button { + padding: 6px 10px; /* Smaller padding for buttons */ + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f0f0f0; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s, color 0.2s; font-size: 14px; - border: none; + min-width: 32px; /* Ensure consistent button width */ + text-align: center; +} + +.pagination button:hover:not(:disabled) { + background-color: #e0e0e0; + border-color: #a0a0a0; +} + +.pagination button:disabled { + cursor: not-allowed; + opacity: 0.6; + background-color: #f8f8f8; + border-color: #e0e0e0; + color: #a0a0a0; +} + +.pagination button.activePage { + background-color: #007bff; + color: white; + border-color: #007bff; } /* MODALS */ diff --git a/src/components/Home.tsx b/src/components/Home.tsx index d81f655..c81aaaa 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -6,6 +6,8 @@ import styles from '../Home.module.css'; import { encodeQueryParams, usePosts } from '../contexts/PostContext'; import InternCard from './InternCard'; +type OpenDropdownType = 'rec' | 'dm' | 'new' | null; + const Home = () => { const DOMAINS: string[] = [ 'FINTECH', @@ -21,23 +23,19 @@ const Home = () => { const DEV: string[] = ['FRONT', 'APP', 'BACKEND', 'DATA', 'OTHERS']; - const { posts, isLoading, fetchPosts } = usePosts(); + const { posts, isLoading, fetchPosts, paginator } = usePosts(); const [panelExpanded, setPanelExpanded] = useState(false); const [roles, setRoles] = useState([]); - const [recExpanded, setRecExpanded] = useState(false); const [isActive, setIsActive] = useState(false); - // radio selection state (temporary) const [recStatus, setRecStatus] = useState<'all' | 'recruiting'>( isActive ? 'recruiting' : 'all' ); const [domains, setDomain] = useState(DOMAINS); const [dmStatus, setDmStatus] = useState(domains); - const [dmExpanded, setDmExpanded] = useState(false); const [order, setOrder] = useState(0); - const [newExpanded, setNewExpanded] = useState(false); const [orderStatus, setOrderStatus] = useState(0); const [query, setQuery] = useState(''); @@ -45,6 +43,25 @@ const Home = () => { const [isModalOpen, setIsModalOpen] = useState(false); const navigate = useNavigate(); + const [openDropdown, setOpenDropdown] = useState(null); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + // const postsPerPage = 12; + + const currentPosts = posts; + const totalPages = paginator; + + const handlePageChange = (pageNumber: number) => { + if (pageNumber > 0 && pageNumber <= totalPages) { + setCurrentPage(pageNumber); + } + }; + + const handleDropdownToggle = (dropdown: OpenDropdownType) => { + setOpenDropdown((prev) => (prev === dropdown ? null : dropdown)); + }; + const handleLoginRequired = () => { setIsModalOpen(true); }; @@ -60,12 +77,13 @@ const Home = () => { params: { roles: roles.length === 0 ? null : roles, isActive: isActive, - domains: domains.length === DOMAINS.length ? null : domains, + domains: domains.length === DOMAINS.length ? null : domains, // if null -> all domains order: order, + page: currentPage, }, }); setQuery(q); - }, [roles, isActive, domains, order]); + }, [roles, isActive, domains, order, currentPage]); // role check const handleRoleCheck = (e: ChangeEvent) => { const { checked, value } = e.currentTarget; @@ -82,7 +100,11 @@ const Home = () => { const handleDomainCheck = (e: ChangeEvent) => { const { checked, value } = e.currentTarget; if (value === 'all') { - setDmStatus(DOMAINS); + if (checked) { + setDmStatus(DOMAINS); + } else { + setDmStatus([]); + } } else { setDmStatus((p) => { if (checked) return [...p, value]; @@ -103,415 +125,480 @@ const Home = () => { setOrderStatus(0); }; - return ( -
    - - {panelExpanded && ( -
    -
    -

    개발

    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -

    기획

    -
    - - -
    -
    - -
    -

    디자인

    -
    - - -
    -
    -
    -

    마케팅

    -
    - - -
    -
    -
    - )} + const renderPageNumbers = () => { + const pageNumbers = []; + const pageBlockSize = 5; -
      - {/* 모집 상태 옵션 버튼 */} -
    • { - setRecExpanded((p) => !p); - }} + const currentBlock = Math.ceil(currentPage / pageBlockSize); + + const startPage = (currentBlock - 1) * pageBlockSize + 1; + const endPage = startPage + pageBlockSize - 1; + + for (let i = startPage; i <= endPage; i++) { + const isDisabled = i > totalPages; + + pageNumbers.push( + - -
    -
    - )} - + {i} + + ); + } + return pageNumbers; + }; - {/* 업종 옵션 버튼 */} -
  • +
    + + {panelExpanded && ( +
    +
    +

    개발

    +
    + +
    -
    - - +
    + +
    -
    - )} -
  • +
    + + +
    +
    + + +
    +
    + + +
    + - {/* 순서 */} -
  • { - setNewExpanded((p) => !p); - }} - > - 최신순 {newExpanded ? : } - {/* 순서 선택 팝업 */} - {newExpanded && ( -
    e.stopPropagation()}> -
    - - +
    +

    기획

    +
    + +
    -
    - - +
    + +
    +

    디자인

    +
    + +
    -
    - )} -
  • -
  • - 초기화 -
  • - - -
    - {isLoading ? ( -

    Loading...

    - ) : ( - posts?.map((post) => ( - - )) +
    +
    +

    마케팅

    +
    + + +
    +
    + )} - + - {isModalOpen && ( -
    setIsModalOpen(false)} - > -
    e.stopPropagation()} - > -

    로그인이 필요한 기능입니다.

    -
    - + +
    +
    + )} + + + {/* 업종 옵션 버튼 */} +
  • handleDropdownToggle('dm')} + > + 업종{' '} + {openDropdown === 'dm' ? : } + {/* 업종 선택 팝업 */} + {openDropdown === 'dm' && ( +
    e.stopPropagation()} + > +
    +
    + + dmStatus.includes(d)) + } + onChange={handleDomainCheck} + /> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + )} +
  • + + {/* 순서 */} +
  • handleDropdownToggle('new')} + > + 최신순{' '} + {openDropdown === 'new' ? : } + {/* 순서 선택 팝업 */} + {openDropdown === 'new' && ( +
    e.stopPropagation()} + > +
    + + +
    +
    + + +
    +
    + )} +
  • +
  • + 초기화 +
  • + + +
    + {isLoading ? ( +

    Loading...

    + ) : ( + //test + currentPosts?.map((post) => ( + + )) + )} +
    + +
    + -
    - )} - + {isModalOpen && ( +
    setIsModalOpen(false)} + > +
    e.stopPropagation()} + > +

    로그인이 필요한 기능입니다.

    +
    + + +
    +
    +
    + )} + + ); }; diff --git a/src/contexts/PostContext.tsx b/src/contexts/PostContext.tsx index d80f71e..2498f62 100644 --- a/src/contexts/PostContext.tsx +++ b/src/contexts/PostContext.tsx @@ -108,7 +108,7 @@ export const PostProvider = ({ children }: PostProviderProps) => { try { const url = `/api/post?${query}`; const response = await apiClient.get<{ - posts: ApiPost[]; + posts: ApiPost[]; // posts stored in array paginator: { lastpage: number }; }>(url); setPaginator(response.data.paginator.lastpage); From 4d63d0acfc3d1fefe8786b2de34501d90bea1ae7 Mon Sep 17 00:00:00 2001 From: Jeongyeon Lee Date: Mon, 3 Nov 2025 15:14:06 +0900 Subject: [PATCH 12/14] Intern component (#12) --- src/Home.module.css | 56 +++++++++++++++++++---------------- src/InternCard.module.css | 5 ++++ src/components/Home.tsx | 10 +++++-- src/components/InternCard.tsx | 23 ++++++++++++-- src/contexts/PostContext.tsx | 4 +-- 5 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/Home.module.css b/src/Home.module.css index 0a2c8dd..3168c94 100644 --- a/src/Home.module.css +++ b/src/Home.module.css @@ -1,24 +1,30 @@ +/* --- Main Layout --- */ .filterContainer { position: sticky; top: 0; z-index: 100; - background-color: white; /* Ensure background is white to cover content when sticky */ - padding: 16px 30px 0px 30px; /* Add padding to match main content */ - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Optional: Add shadow for better visual separation */ + background-color: white; + padding: 16px 30px 0px 30px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .main { display: flex; flex-direction: column; - padding: 0 30px 16px 30px; /* Adjust padding for main content */ + padding: 0 30px 16px 30px; } .contentWrapper { display: flex; flex-direction: column; gap: 20px; + width: 100%; + /* Removed max-width for 3-column layout */ + margin: 0 auto; + width: 1000px; } +/* --- Filter Panel (Accordion) --- */ .accordion { background-color: #eee; color: #444; @@ -46,6 +52,7 @@ overflow: hidden; } +/* --- Filter Chips --- */ ul.chips { margin: 0; list-style: none; @@ -55,43 +62,47 @@ ul.chips { padding: 0; } +/* --- Post Grid (3xN Layout) --- */ .postList { display: grid; + /* Defines the 3-column layout */ grid-template-columns: repeat(3, 1fr); - gap: 20px; /* Gap between cards */ + gap: 20px; } +/* --- Pagination --- */ .pagination { display: flex; justify-content: center; align-items: center; - gap: 5px; /* Smaller gap between page buttons */ + gap: 5px; margin-top: 20px; } .pagination button { - padding: 6px 10px; /* Smaller padding for buttons */ - border: 1px solid #ccc; + padding: 6px 10px; + border: 1px solid #dee2e6; border-radius: 4px; - background-color: #f0f0f0; + background-color: #ffffff; + color: #212529; cursor: pointer; transition: background-color 0.2s, border-color 0.2s, color 0.2s; font-size: 14px; - min-width: 32px; /* Ensure consistent button width */ + min-width: 32px; text-align: center; } .pagination button:hover:not(:disabled) { - background-color: #e0e0e0; - border-color: #a0a0a0; + background-color: #f8f9fa; + border-color: #adb5bd; } .pagination button:disabled { cursor: not-allowed; opacity: 0.6; - background-color: #f8f8f8; - border-color: #e0e0e0; - color: #a0a0a0; + background-color: #f8f9fa; + border-color: #e9ecef; + color: #adb5bd; } .pagination button.activePage { @@ -100,7 +111,7 @@ ul.chips { border-color: #007bff; } -/* MODALS */ +/* --- Modal Styles (Dropdown) --- */ .modalTrigger { position: relative; } @@ -136,8 +147,6 @@ ul.chips { cursor: pointer; } -/* If your JSX uses a plain `className="row"`, define that globally in `src/style.css`. - Prefer using `styles.row` (CSS modules) instead; see `Home.tsx` for where .row is used. */ .modalActions { display: flex; justify-content: flex-end; @@ -176,7 +185,6 @@ ul.chips { transition: all 0.2s; } .radioVisual::before { - /* inner dot */ content: ""; width: 10px; height: 10px; @@ -192,11 +200,7 @@ ul.chips { transform: scale(1); } -/* input[type="checkbox"] { - -} */ - -/* Login Confirmation Modal Styles */ +/* --- Modal Styles (Login Confirmation) --- */ .modalBackdrop { position: fixed; top: 0; @@ -239,12 +243,12 @@ ul.chips { cursor: pointer; font-size: 14px; font-weight: 500; - color: #212529; /* Dark text color for better visibility */ + color: #212529; transition: background-color 0.2s; } .modalActions button:first-child:hover { - background-color: #e2e6ea; /* Light grey on hover */ + background-color: #e2e6ea; } .modalActions button:last-child { diff --git a/src/InternCard.module.css b/src/InternCard.module.css index 8580225..c11b316 100644 --- a/src/InternCard.module.css +++ b/src/InternCard.module.css @@ -9,6 +9,7 @@ display: flex; flex-direction: column; gap: 12px; + width: 100%; } .card:hover { @@ -30,6 +31,10 @@ .titleGroup { flex: 1; + display: flex; + justify-content: center; + flex-direction: column; + margin-top: 10px; } .companyName { diff --git a/src/components/Home.tsx b/src/components/Home.tsx index c81aaaa..df8c5d8 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -79,7 +79,7 @@ const Home = () => { isActive: isActive, domains: domains.length === DOMAINS.length ? null : domains, // if null -> all domains order: order, - page: currentPage, + page: currentPage - 1, }, }); setQuery(q); @@ -95,6 +95,7 @@ const Home = () => { return p.filter((v) => v !== value); }); } + setCurrentPage(1); }; // domain check const handleDomainCheck = (e: ChangeEvent) => { @@ -116,13 +117,13 @@ const Home = () => { const handleReset = () => { // reset 모집 상태 selection and other filters setIsActive(false); - setRoles([]); setDomain(DOMAINS); setOrder(0); setRecStatus('all'); setDmStatus(DOMAINS); setOrderStatus(0); + setCurrentPage(1); }; const renderPageNumbers = () => { @@ -188,7 +189,7 @@ const Home = () => { { onClick={() => { setIsActive(recStatus === 'recruiting'); setOpenDropdown(null); + setCurrentPage(1); }} > 적용 @@ -473,6 +475,7 @@ const Home = () => { onClick={() => { setDomain(dmStatus); setOpenDropdown(null); + setCurrentPage(1); }} > 적용 @@ -531,6 +534,7 @@ const Home = () => { onClick={() => { setOrder(orderStatus); setOpenDropdown(null); + setCurrentPage(1); }} > 적용 diff --git a/src/components/InternCard.tsx b/src/components/InternCard.tsx index 6d1bddb..5ccee25 100644 --- a/src/components/InternCard.tsx +++ b/src/components/InternCard.tsx @@ -13,6 +13,18 @@ const InternCard = ({ post, onLoginRequired }: InternCardProps) => { const { user } = useAuth(); const { toggleBookmark } = usePosts(); + const DOMAIN_MAP: { [key: string]: string } = { + FINTECH: '핀테크', + HEALTHTECH: '헬스테크', + EDUCATION: '교육', + ECOMMERCE: '이커머스', + FOODTECH: '푸드테크', + MOBILITY: '모빌리티', + CONTENTS: '컨텐츠', + B2B: 'B2B', + OTHERS: '기타', + }; + const handleBookmarkClick = async () => { if (!user) { onLoginRequired(); @@ -51,8 +63,8 @@ const InternCard = ({ post, onLoginRequired }: InternCardProps) => {

    {post.companyName}

    -

    {post.positionTitle}

    +
    +
    +

    {post.positionTitle}

    +
    - {post.domain} - {post.positionType} + + {DOMAIN_MAP[post.domain] || post.domain} + + {/* {post.positionType} */}
    diff --git a/src/contexts/PostContext.tsx b/src/contexts/PostContext.tsx index 2498f62..cba1fe5 100644 --- a/src/contexts/PostContext.tsx +++ b/src/contexts/PostContext.tsx @@ -109,9 +109,9 @@ export const PostProvider = ({ children }: PostProviderProps) => { const url = `/api/post?${query}`; const response = await apiClient.get<{ posts: ApiPost[]; // posts stored in array - paginator: { lastpage: number }; + paginator: { lastPage: number }; }>(url); - setPaginator(response.data.paginator.lastpage); + setPaginator(response.data.paginator.lastPage); const apiPosts = response.data.posts; const formattedPosts: Post[] = apiPosts.map((p: ApiPost) => ({ id: p.id, From f49392921e55d6b3b1c085fa1e36b7643f3455d0 Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Mon, 3 Nov 2025 20:50:32 +0900 Subject: [PATCH 13/14] fix: change layout from grid to flexbox, moderate css changes --- src/Home.module.css | 128 +++++++++++++-------- src/components/Home.tsx | 245 ++++++++++++++++++++-------------------- 2 files changed, 203 insertions(+), 170 deletions(-) diff --git a/src/Home.module.css b/src/Home.module.css index 3168c94..a7ceda6 100644 --- a/src/Home.module.css +++ b/src/Home.module.css @@ -1,30 +1,33 @@ -/* --- Main Layout --- */ -.filterContainer { - position: sticky; - top: 0; - z-index: 100; - background-color: white; - padding: 16px 30px 0px 30px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} .main { display: flex; flex-direction: column; - padding: 0 30px 16px 30px; + padding: 16px 30px; +} +.panelWrapper { + flex-direction: column; + align-items: flex-start; } .contentWrapper { display: flex; - flex-direction: column; + flex-direction: row; gap: 20px; width: 100%; - /* Removed max-width for 3-column layout */ margin: 0 auto; - width: 1000px; + max-width: 1200px; + padding: 0 16px; +} + +.filterContainer { + flex: 0 0 220px; /* Do not grow, do not shrink, base width 220px */ +} + +.postsArea { + flex: 1; /* Take up remaining space */ + min-width: 0; /* Prevent overflow */ } -/* --- Filter Panel (Accordion) --- */ .accordion { background-color: #eee; color: #444; @@ -52,25 +55,61 @@ overflow: hidden; } -/* --- Filter Chips --- */ +.panel div { + margin-bottom: 6px; + font-size: 18px; +} +.panel section { + margin-top: 12px; +} + ul.chips { - margin: 0; + margin: 30px 0 50px; list-style: none; display: flex; gap: 6px; flex-wrap: wrap; - padding: 0; + padding: 0 16px 16px 16px; } -/* --- Post Grid (3xN Layout) --- */ +/* --- Post List (flex, responsive) --- */ .postList { - display: grid; - /* Defines the 3-column layout */ - grid-template-columns: repeat(3, 1fr); + display: flex; + flex-wrap: wrap; gap: 20px; + justify-content: space-between; + align-items: flex-start; +} + +.postList > * { + /* Default: 3 columns. The -20px accounts for gaps so cards fit nicely */ + flex: 1 1 calc(33.333% - 20px); + max-width: calc(33.333% - 20px); +} + +/* Medium screens: 2 columns */ +@media (max-width: 1000px) { + .postList > * { + flex: 1 1 calc(50% - 20px); + max-width: calc(50% - 20px); + } +} + +/* Small screens: 1 column */ +@media (max-width: 768px) { + .contentWrapper { + flex-direction: column; + padding: 0 12px; + } + .postList { + gap: 16px; + } + .postList > * { + flex: 1 1 100%; + max-width: 100%; + } } -/* --- Pagination --- */ .pagination { display: flex; justify-content: center; @@ -106,12 +145,11 @@ ul.chips { } .pagination button.activePage { - background-color: #007bff; + background-color: #555; color: white; - border-color: #007bff; + border-color: rgba(217, 217, 217, 1); } -/* --- Modal Styles (Dropdown) --- */ .modalTrigger { position: relative; } @@ -200,6 +238,22 @@ ul.chips { transform: scale(1); } +[type="checkbox"] { + appearance: none; /* 기본(네이티브) 모양을 제거 */ + box-sizing: border-box; + background-clip: content-box; + width: 1.25em; + height: 1.25em; + border: 1px solid rgba(72, 76, 83, 1); + border-radius: 3px; + cursor: pointer; +} +[type="checkbox"]:checked { + border-color: rgba(72, 76, 83, 1); + background-color: rgba(72, 76, 83, 1); +} + + /* --- Modal Styles (Login Confirmation) --- */ .modalBackdrop { position: fixed; @@ -235,28 +289,4 @@ ul.chips { justify-content: center; } -.modalActions button { - padding: 10px 20px; - border-radius: 6px; - border: 1px solid #ccc; - background-color: #f0f0f0; - cursor: pointer; - font-size: 14px; - font-weight: 500; - color: #212529; - transition: background-color 0.2s; -} - -.modalActions button:first-child:hover { - background-color: #e2e6ea; -} -.modalActions button:last-child { - background-color: #007bff; - color: white; - border-color: #007bff; -} - -.modalActions button:last-child:hover { - background-color: #0069d9; -} diff --git a/src/components/Home.tsx b/src/components/Home.tsx index df8c5d8..332548c 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -2,7 +2,6 @@ import { type ChangeEvent, useEffect, useState } from 'react'; import { FaAngleDown, FaAngleUp, FaArrowRotateRight } from 'react-icons/fa6'; import { useNavigate } from 'react-router-dom'; import styles from '../Home.module.css'; -// import { useAuth } from '../contexts/AuthContext'; import { encodeQueryParams, usePosts } from '../contexts/PostContext'; import InternCard from './InternCard'; @@ -84,11 +83,13 @@ const Home = () => { }); setQuery(q); }, [roles, isActive, domains, order, currentPage]); + // role check const handleRoleCheck = (e: ChangeEvent) => { const { checked, value } = e.currentTarget; if (value === 'total') { - setRoles((p) => [...DEV, ...p.filter((v) => !DEV.includes(v))]); + if (checked) setRoles((p) => [...DEV, ...p.filter((v) => !DEV.includes(v))]); + else setRoles((p) => [...p.filter((v) => !DEV.includes(v))]); } else { setRoles((p) => { if (checked) return [...p, value]; @@ -97,6 +98,7 @@ const Home = () => { } setCurrentPage(1); }; + // domain check const handleDomainCheck = (e: ChangeEvent) => { const { checked, value } = e.currentTarget; @@ -157,123 +159,122 @@ const Home = () => { return ( <> -
    - - {panelExpanded && ( -
    -
    -

    개발

    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -

    기획

    -
    - - -
    -
    - -
    -

    디자인

    -
    - - -
    -
    -
    -

    마케팅

    -
    - - +
    +
    +
    + + {panelExpanded && ( +
    +
    +

    개발

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +

    기획

    +
    + + +
    +
    + +
    +

    디자인

    +
    + + +
    +
    +
    +

    마케팅

    +
    + + +
    +
    -
    + )}
    - )} -
    - -
    -
    -
    +
      {/* 모집 상태 옵션 버튼 */}
    • {
    {renderPageNumbers()} -
    From d9092dcd64d77bd2e4161b83c6687834d1efc701 Mon Sep 17 00:00:00 2001 From: Seoyeon HEO Date: Mon, 3 Nov 2025 21:45:54 +0900 Subject: [PATCH 14/14] fix: css format issues --- src/Home.module.css | 33 +++++++++++++++++---------------- src/components/Home.tsx | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/Home.module.css b/src/Home.module.css index a7ceda6..134d82e 100644 --- a/src/Home.module.css +++ b/src/Home.module.css @@ -1,4 +1,3 @@ - .main { display: flex; flex-direction: column; @@ -24,8 +23,8 @@ } .postsArea { - flex: 1; /* Take up remaining space */ - min-width: 0; /* Prevent overflow */ + flex: 1; + min-width: 0; } .accordion { @@ -239,21 +238,25 @@ ul.chips { } [type="checkbox"] { - appearance: none; /* 기본(네이티브) 모양을 제거 */ - box-sizing: border-box; - background-clip: content-box; - width: 1.25em; - height: 1.25em; - border: 1px solid rgba(72, 76, 83, 1); - border-radius: 3px; - cursor: pointer; + appearance: none; + box-sizing: border-box; + background-clip: content-box; + width: 1.25em; + height: 1.25em; + border: 1px solid rgba(72, 76, 83, 1); + border-radius: 3px; + cursor: pointer; } + [type="checkbox"]:checked { - border-color: rgba(72, 76, 83, 1); - background-color: rgba(72, 76, 83, 1); + border-color: rgba(72, 76, 83, 1); + background-color: rgba(72, 76, 83, 1); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 8' fill='none'%3E%3Cpath d='M1 3.5L4.5 7L9.5 1' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: 0.75em; } - /* --- Modal Styles (Login Confirmation) --- */ .modalBackdrop { position: fixed; @@ -288,5 +291,3 @@ ul.chips { gap: 12px; justify-content: center; } - - diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 332548c..6e804cd 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -88,7 +88,8 @@ const Home = () => { const handleRoleCheck = (e: ChangeEvent) => { const { checked, value } = e.currentTarget; if (value === 'total') { - if (checked) setRoles((p) => [...DEV, ...p.filter((v) => !DEV.includes(v))]); + if (checked) + setRoles((p) => [...DEV, ...p.filter((v) => !DEV.includes(v))]); else setRoles((p) => [...p.filter((v) => !DEV.includes(v))]); } else { setRoles((p) => { @@ -594,10 +595,18 @@ const Home = () => { >

    로그인이 필요한 기능입니다.

    - -