diff --git a/README.md b/README.md index b7e08e30..dd7eb651 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ -# 워크루트(WorkRoot) 프로젝트 +# 코드잇 프론트엔드 9기 2팀 워크루트(WorkRoot) 프로젝트 -## 소개 +
+ +
-**워크루트(WorkRoot) 프로젝트**는 Next.js, React, TypeScript, TailwindCSS 등 최신 기술 스택을 기반으로 한 프로젝트입니다. +## 배포 사이트: https://www.workroot.life + +## 프로젝트 소개 + +- WorkRoot 소개: 🌳 "일"을 통해 자신의 뿌리를 내리며 "성장"하는 구인구직 사이트 +- 개발 기간: 2024. 11. 21 ~ 2024. 12. 30 +- 개발 인원: 4명 + +| 김원 | 김예지 | 김태준 | 홍예림 | +| :---------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | +| | | | | +| cccwon2 | yyezzzy | imtaejunk | hongggyelim | + +- 역할 + - 김원: 팀장, 레포지토리 세팅, 로그인/회원가입, 마이페이지, 공고 목록 페이지, 게시판 목록 페이지, 공고 카드 컴포넌트, 모달 컴포넌트, SEO 설정, 지도 API 연동, Supabase 연동, 회의록 작성 + - 김예지: 소셜로그인/회원가입, 알바폼 상세페이지, 로고, 랜딩페이지 디자인 및 기획, 공통 버튼 컴포넌트, 스토리북 세팅, 크로매틱 배포 세팅 + - 김태준: 랜딩 페이지, 게시판 상세 페이지, 게시판 카드 컴포넌트, 전역 에러 페이지, 404 페이지 + - 홍예림: 공고 작성 페이지, 공고 수정 페이지, 공통 인풋 컴포넌트, 데이터피커 커스텀, 마우스 커서 커스텀, 깃헙 액션 CI 세팅 ## 기술 스택 @@ -10,7 +29,7 @@ [![Next.js](https://img.shields.io/badge/Next.js-14.2.15-black?logo=next.js)](https://nextjs.org/) [![React](https://img.shields.io/badge/React-18-blue?logo=react)](https://reactjs.org/) -[![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript)](https://www.typescriptlang.org/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.6.3-blue?logo=typescript)](https://www.typescriptlang.org/) [![TailwindCSS](https://img.shields.io/badge/TailwindCSS-3.4.14-38B2AC?logo=tailwind-css)](https://tailwindcss.com/) ### 상태 관리 & 데이터 페칭 @@ -18,6 +37,7 @@ [![React Query](https://img.shields.io/badge/@tanstack/react--query-5.59.19-FF4154?logo=react-query)](https://tanstack.com/query) [![Zustand](https://img.shields.io/badge/Zustand-5.0.1-brown)](https://github.com/pmndrs/zustand) [![Axios](https://img.shields.io/badge/Axios-1.7.7-5A29E4?logo=axios)](https://axios-http.com/) +[![Supabase](https://img.shields.io/badge/Supabase-2.47.10-3ECF8E?logo=supabase)](https://supabase.io/) ### 폼 & 유효성 검사 @@ -25,70 +45,29 @@ [![Zod](https://img.shields.io/badge/Zod-3.23.8-3068B7)](https://zod.dev/) [![HookForm Resolvers](https://img.shields.io/badge/@hookform/resolvers-3.9.0-EC5990)](https://github.com/react-hook-form/resolvers) -### UI 컴포넌트 +### UI/UX -[![React DatePicker](https://img.shields.io/badge/React%20DatePicker-7.4.0-216BA5)](https://reactdatepicker.com/) -[![React Modal](https://img.shields.io/badge/React%20Modal-3.16.1-black)](https://github.com/reactjs/react-modal) +[![Framer Motion](https://img.shields.io/badge/Framer%20Motion-11.15.0-0055FF?logo=framer)](https://www.framer.com/motion/) +[![React DatePicker](https://img.shields.io/badge/React%20DatePicker-7.5.0-216BA5)](https://reactdatepicker.com/) [![React Icons](https://img.shields.io/badge/React%20Icons-5.3.0-E91E63)](https://react-icons.github.io/react-icons) -[![React Spinners](https://img.shields.io/badge/React%20Spinners-0.14.1-36D7B7)](https://www.davidhu.io/react-spinners/) [![React Hot Toast](https://img.shields.io/badge/React%20Hot%20Toast-2.4.1-FF4444)](https://react-hot-toast.com/) +[![Hello Pangea DnD](https://img.shields.io/badge/Hello%20Pangea%20DnD-17.0.0-yellow)](https://github.com/hello-pangea/dnd) + +### 지도 & 소셜 + +[![React Kakao Maps SDK](https://img.shields.io/badge/React%20Kakao%20Maps%20SDK-1.1.27-FFCD00)](https://www.npmjs.com/package/react-kakao-maps-sdk) +[![Next Auth](https://img.shields.io/badge/Next%20Auth-4.24.10-000000?logo=next.js)](https://next-auth.js.org/) +[![Channel Talk](https://img.shields.io/badge/Channel%20Talk%20SDK-2.0.0-00A6B4)](https://channel.io/) ### 개발 도구 [![ESLint](https://img.shields.io/badge/ESLint-8.57.1-4B32C3?logo=eslint)](https://eslint.org/) [![Prettier](https://img.shields.io/badge/Prettier-3.3.3-F7B93E?logo=prettier)](https://prettier.io/) -[![Husky](https://img.shields.io/badge/Husky-9.1.6-yellow?logo=git)](https://typicode.github.io/husky/) -[![Commitlint](https://img.shields.io/badge/Commitlint-19.5.0-green?logo=commitlint)](https://commitlint.js.org/) +[![Storybook](https://img.shields.io/badge/Storybook-8.4.4-FF4785?logo=storybook)](https://storybook.js.org/) +[![React Query DevTools](https://img.shields.io/badge/React%20Query%20DevTools-5.59.20-FF4154)](https://tanstack.com/query/latest/docs/react/devtools) ## 개발 환경 설정 -### 커밋 메시지 컨벤션 - -commitlint를 사용하여 다음과 같은 커밋 메시지 형식을 강제합니다: - -``` -type: Subject -``` - -#### 커밋 타입: - -- feat: 새로운 기능에 대한 커밋 - - 예: feat: 회원가입 기능 구현 -- fix: 버그 수정에 대한 커밋 - - 예: fix: 회원가입 유효성 검사 오류 수정 -- build: 빌드 관련 파일 수정에 대한 커밋 - - 예: build: next.config.js 업데이트 -- chore: 그 외 자잘한 수정에 대한 커밋 - - 예: chore: 불필요한 console.log 제거 -- delete: 기능 삭제에 대한 커밋 - - 예: delete: 사용하지 않는 컴포넌트 제거 -- ci: CI/CD 관련 설정 수정에 대한 커밋 - - 예: ci: github actions workflow 추가 -- docs: 문서 수정에 대한 커밋 - - 예: docs: readme.md 업데이트 -- style: 코드 스타일 혹은 포맷 등에 관한 커밋 - - 예: style: 들여쓰기 수정 -- refactor: 코드 리팩토링에 대한 커밋 - - 예: refactor: 회원가입 로직 개선 -- test: 테스트 코드 수정에 대한 커밋 - - 예: test: 회원가입 테스트 케이스 추가 - -#### 커밋 메시지 규칙: - -- type은 소문자로 시작합니다 -- type은 위 목록 중 하나여야 합니다 -- Subject는 필수이며 비어있을 수 없습니다 -- type과 Subject 사이에는 ': ' (콜론+공백)이 있어야 합니다 - -#### 예시: - -```bash -feat: 회원가입 기능 구현 -fix: 로그인 버튼 클릭 시 오류 수정 -docs: API 문서 업데이트 -style: 코드 포맷팅 적용 -``` - ### ESLint 설정 ```json @@ -98,12 +77,22 @@ style: 코드 포맷팅 적용 "next/core-web-vitals", "eslint:recommended", "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" + "plugin:prettier/recommended", + "prettier" ], "plugins": ["@typescript-eslint", "prettier"], + "parser": "@typescript-eslint/parser", "rules": { - "prettier/prettier": ["error", { "endOfLine": "lf" }], + "import/no-anonymous-default-export": "off", + "@typescript-eslint/no-explicit-any": "off", + "prettier/prettier": [ + "error", + { + "endOfLine": "lf" + } + ], "linebreak-style": ["error", "unix"], + "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "warn" } } @@ -125,73 +114,81 @@ style: 코드 포맷팅 적용 "proseWrap": "preserve", // 마크다운 텍스트의 줄바꿈 처리 방식 "endOfLine": "lf", // 줄 끝 문자를 LF(Line Feed)로 통일 "plugins": [ - "@trivago/prettier-plugin-sort-imports", // 임포트 구문 자동 정렬 플러그인 "prettier-plugin-tailwindcss" // Tailwind CSS 클래스 자동 정렬 플러그인 - ], - "importOrder": [ - // 임포트 구문 정렬 순서 설정 - "^@/lib/(.*)$", // 1순위: lib 디렉토리 임포트 - "^@/app/(.*)$", // 2순위: app 디렉토리 임포트 - "^@/components/(.*)$", // 3순위: components 디렉토리 임포트 - "^[./]" // 4순위: 상대 경로 임포트 - ], - "importOrderSeparation": true, // 임포트 그룹 사이에 빈 줄 추가 - "importOrderSortSpecifiers": true // 임포트 구문 내부의 요소들 정렬 + ] } ``` ### TailwindCSS 설정 ```typescript -const config = { +import type { Config } from "tailwindcss"; + +const config: Config = { content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + "./stories/**/*.{js,ts,jsx,tsx}", + "./src/app/globals.css", + "./src/stories/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { colors: { - background: "var(--background)", foreground: "var(--foreground)", + black: { + 100: "#6B6B6B", + 200: "#525252", + 300: "#373737", + 400: "#1F1F1F", + 500: "#040404", + }, + grayscale: { + 50: "#FFFFFF", + 100: "#DEDEDE", + 200: "#C4C4C4", + 300: "#ABABAB", + 400: "#999999", + 500: "#808080", + }, + primary: { + orange: { + 50: "#fbfffd", + 100: "#8ab08c", + 200: "#64a466", + 300: "#388e3c", + 400: "#156719", + }, + blue: { + 100: "#535779", + 200: "#3E415B", + 300: "#2A2C3D", + }, + }, + background: { + 100: "#FCFCFC", + 200: "#F7F7F7", + 300: "#EFEFEF", + }, + line: { + 100: "#F2F2F2", + 200: "#E6E6E6", + }, + state: { + error: "#FC4100", + }, }, fontFamily: { - sans: [ - "Pretendard", - "-apple-system", - "BlinkMacSystemFont", - "system-ui", - "Helvetica Neue", - "Apple SD Gothic Neo", - "sans-serif", - ], + nexon: ["var(--font-nexon)", "sans-serif"], + hakgyo: ["var(--font-hakgyo)", "sans-serif"], + sans: ["Pretendard", "sans-serif"], }, }, }, }; -``` -### Git Hooks (Husky) - -- pre-commit: lint-staged를 실행하여 커밋 전 코드 품질 검사 -- commit-msg: commitlint를 통한 커밋 메시지 형식 검사 - -### 스크립트 - -```json -{ - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "clean": "rimraf .next out", - "prepare": "husky", - "lint-staged": "lint-staged", - "format": "prettier --write .", - "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix" - } -} +export default config; ``` ## 시작하기 @@ -206,8 +203,45 @@ git clone [repository-url] 프로젝트 루트에 .env 파일을 생성하고 다음 내용을 추가합니다: ```bash +# 클라이언트 사이드 NEXT_PUBLIC_API_URL=your-api-url NEXT_PUBLIC_TEAM_ID=your-team-id + +# 서버 사이드 +CHROMATIC_PROJECT_TOKEN=your-chromatic-project-token + +# 도메인 +NEXT_PUBLIC_DOMAIN_URL=your-domain-url + +# 테스트 계정(지원자) +NEXT_PUBLIC_TEST_APPLICANT_ID=your-test-applicant-id +NEXT_PUBLIC_TEST_APPLICANT_PASSWORD=your-test-applicant-password + +# 테스트 계정(사장님) +NEXT_PUBLIC_TEST_OWNER_ID=your-test-owner-id +NEXT_PUBLIC_TEST_OWNER_PASSWORD=your-test-owner-password + +# 페이스북 +NEXT_PUBLIC_FB_APP_ID=your-facebook-app-id + +# 카카오맵 +NEXT_PUBLIC_KAKAO_APP_KEY=your-kakao-app-key + +# 수파베이스 +NEXT_PUBLIC_SUPABASE_URL=your-supabase-url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key + +# 카카오 클라이언트 아이디 +NEXT_PUBLIC_KAKAO_CLIENT_ID=your-kakao-client-id + +# 구글 클라이언트 아이디 +NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id + +# 채널톡 플러그인 키 +NEXT_PUBLIC_CHANNEL_IO_PLUGIN_KEY=your-channel-io-plugin-key + +# 구글 애널리틱스 +NEXT_PUBLIC_GA_ID=your-google-analytics-id ``` 3. 의존성 설치: @@ -226,13 +260,30 @@ npm run dev ``` src/ -├── app/ # Next.js 14+ App Router -├── components/ # 재사용 가능한 컴포넌트 -├── hooks/ # 커스텀 훅 -├── lib/ # 유틸리티 함수 -├── store/ # 상태 관리 -├── types/ # TypeScript 타입 정의 -└── zod/ # Zod 스키마 정의 +├── app/ # Next.js 14+ App Router +│ ├── (auth)/ # 인증 관련 라우트 (로그인, 회원가입) +│ ├── (home)/ # 홈 페이지 관련 라우트 +│ ├── (pages)/ # 주요 페이지 라우트 +│ │ ├── (workform)/ # 워크폼 관련 페이지 +│ │ ├── my-workform/ # 마이 워크폼 페이지 +│ │ └── work-talk/ # 워크톡 게시판 페이지 +│ └── components/ # 공통 컴포넌트 +│ ├── animation/ # 애니메이션 컴포넌트 +│ ├── button/ # 버튼 컴포넌트 +│ ├── card/ # 카드 컴포넌트 +│ ├── chip/ # 칩 컴포넌트 +│ ├── input/ # 입력 컴포넌트 +│ ├── layout/ # 레이아웃 컴포넌트 +│ ├── loading-spinner/ # 로딩 스피너 컴포넌트 +│ └── pagination/ # 페이지네이션 컴포넌트 +├── constants/ # 상수 정의 +├── hooks/ # 커스텀 훅 +│ └── queries/ # React Query 관련 훅 +├── lib/ # 유틸리티 함수 +├── schemas/ # Zod 스키마 정의 +├── store/ # 상태 관리 (Zustand) +├── types/ # TypeScript 타입 정의 +└── utils/ # 유틸리티 함수 ``` ## 라이선스 diff --git a/package-lock.json b/package-lock.json index 00307eff..252c7083 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@channel.io/channel-web-sdk-loader": "^2.0.0", + "@hello-pangea/dnd": "^17.0.0", "@hookform/resolvers": "^3.9.0", "@lottiefiles/react-lottie-player": "^3.5.4", "@next/third-parties": "^15.1.2", @@ -24,7 +25,6 @@ "next-sitemap": "^4.2.3", "prettier": "^3.3.3", "react": "^18", - "react-beautiful-dnd": "^13.1.1", "react-datepicker": "^7.5.0", "react-dom": "^18", "react-hook-form": "^7.53.0", @@ -2574,6 +2574,25 @@ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", "license": "MIT" }, + "node_modules/@hello-pangea/dnd": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-17.0.0.tgz", + "integrity": "sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.25.6", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^9.1.2", + "redux": "^5.0.1", + "use-memo-one": "^1.1.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@hookform/resolvers": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", @@ -7029,16 +7048,6 @@ "@types/send": "*" } }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", - "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -7107,6 +7116,7 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -7127,6 +7137,7 @@ "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -7176,18 +7187,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-redux": { - "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", - "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", - "license": "MIT", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -7225,6 +7224,12 @@ "@types/send": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -11911,21 +11916,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -13143,9 +13133,9 @@ } }, "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, "node_modules/memoizerific": { @@ -14882,26 +14872,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "deprecated": "react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-confetti": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz", @@ -15049,6 +15019,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, "license": "MIT" }, "node_modules/react-kakao-maps-sdk": { @@ -15066,26 +15037,24 @@ } }, "node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" }, "peerDependenciesMeta": { - "react-dom": { + "@types/react": { "optional": true }, - "react-native": { + "redux": { "optional": true } } @@ -15298,13 +15267,10 @@ } }, "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.9", @@ -17483,6 +17449,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/package.json b/package.json index 9e2f8f93..89bb857d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@channel.io/channel-web-sdk-loader": "^2.0.0", + "@hello-pangea/dnd": "^17.0.0", "@hookform/resolvers": "^3.9.0", "@lottiefiles/react-lottie-player": "^3.5.4", "@next/third-parties": "^15.1.2", @@ -30,7 +31,6 @@ "next-sitemap": "^4.2.3", "prettier": "^3.3.3", "react": "^18", - "react-beautiful-dnd": "^13.1.1", "react-datepicker": "^7.5.0", "react-dom": "^18", "react-hook-form": "^7.53.0", diff --git a/src/app/(pages)/(workform)/addform/page.tsx b/src/app/(pages)/(workform)/addform/page.tsx index 5ebb55e5..ebba23b4 100644 --- a/src/app/(pages)/(workform)/addform/page.tsx +++ b/src/app/(pages)/(workform)/addform/page.tsx @@ -1,4 +1,5 @@ "use client"; + import { useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { FormProvider, useForm } from "react-hook-form"; diff --git a/src/app/(pages)/(workform)/addform/section/RecruitContentSection.tsx b/src/app/(pages)/(workform)/addform/section/RecruitContentSection.tsx index 1c6f360f..402bc344 100644 --- a/src/app/(pages)/(workform)/addform/section/RecruitContentSection.tsx +++ b/src/app/(pages)/(workform)/addform/section/RecruitContentSection.tsx @@ -1,4 +1,5 @@ "use client"; + import BaseInput from "@/app/components/input/text/BaseInput"; import BaseTextArea from "@/app/components/input/textarea/BaseTextArea"; import DatePickerInput from "@/app/components/input/dateTimeDaypicker/DatePickerInput"; @@ -10,7 +11,6 @@ import { ImageInputType } from "@/types/addform"; import useUploadImages from "@/hooks/queries/user/me/useImageUpload"; import { formatToLocaleDate } from "@/utils/formatters"; import DotLoadingSpinner from "@/app/components/loading-spinner/DotLoadingSpinner"; -import { isDirty } from "zod"; import ImageInput from "@/app/components/input/file/ImageInput/ImageInput"; // 워크폼 만들기 - 사장님 - 1-모집내용 diff --git a/src/app/(pages)/work-talk/[talkId]/edit/page.tsx b/src/app/(pages)/work-talk/[talkId]/edit/page.tsx index 9d699585..704cbce4 100644 --- a/src/app/(pages)/work-talk/[talkId]/edit/page.tsx +++ b/src/app/(pages)/work-talk/[talkId]/edit/page.tsx @@ -1,11 +1,10 @@ "use client"; import React, { useState, useCallback, useEffect } from "react"; -import { useForm, SubmitHandler, Controller } from "react-hook-form"; +import { useForm, SubmitHandler, Controller, FormProvider } from "react-hook-form"; import { useRouter } from "next/navigation"; import Button from "@/app/components/button/default/Button"; import BaseInput from "@/app/components/input/text/BaseInput"; -import ImageInputPlaceHolder from "@/app/components/input/file/ImageInput/ImageInputPlaceHolder"; import { useEditPost } from "@/hooks/queries/post/useEditPost"; import { usePostDetail } from "@/hooks/queries/post/usePostDetail"; import axios from "axios"; @@ -14,29 +13,11 @@ import { PostSchema } from "@/schemas/postSchema"; import toast from "react-hot-toast"; import LoadingSpinner from "@/app/components/loading-spinner/LoadingSpinner"; import { useQueryClient } from "@tanstack/react-query"; - -interface ImageInputType { - file: File | null; - url: string; - id: string; -} +import ImageInput from "@/app/components/input/file/ImageInput/ImageInput"; +import { ImageInputType } from "@/types/addform"; export default function EditTalk({ params }: { params: { talkId: string } }) { - const [imageList, setImageList] = useState([]); - const router = useRouter(); - const postId = params.talkId; - - const { data: post, isLoading, error } = usePostDetail(postId); - const { mutate: editPost, isPending } = useEditPost(postId); - const queryClient = useQueryClient(); - - const { - control, - handleSubmit, - setValue, - formState: { errors }, - reset, - } = useForm({ + const methods = useForm({ defaultValues: { title: "", content: "", @@ -44,6 +25,23 @@ export default function EditTalk({ params }: { params: { talkId: string } }) { }, }); + const { + control, + handleSubmit, + formState: { errors }, + reset, + setValue, + } = methods; + + const [isUploading, setIsUploading] = useState(false); + const [uploadedImages, setUploadedImages] = useState([]); + const router = useRouter(); + const postId = params.talkId; + + const { data: post, isLoading, error } = usePostDetail(postId); + const { mutate: editPost, isPending } = useEditPost(postId); + const queryClient = useQueryClient(); + // 게시글 데이터로 폼 초기화 useEffect(() => { if (post) { @@ -53,16 +51,13 @@ export default function EditTalk({ params }: { params: { talkId: string } }) { imageUrl: post.imageUrl || "", }); + // 이미지 초기화 if (post.imageUrl) { - setImageList([ - { - file: null, - url: post.imageUrl, - id: "initial-image", - }, - ]); - } else { - setImageList([]); + const initialImages = post.imageUrl.split(",").map((url) => ({ + url, + id: crypto.randomUUID(), + })); + setUploadedImages(initialImages); } } }, [post, reset]); @@ -81,48 +76,31 @@ export default function EditTalk({ params }: { params: { talkId: string } }) { try { const response = await axios.post("/api/images/upload", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, + headers: { "Content-Type": "multipart/form-data" }, }); - - if (response.status === 201 && response.data?.url) { - return response.data.url; - } - - throw new Error("이미지 업로드에 실패했습니다."); + return response.data.url; } catch (error) { - toast.error("이미지 업로드에 실패했습니다."); console.error("이미지 업로드 실패:", error); throw error; } }, []); - const handleImagesChange = useCallback( - (newImages: ImageInputType[]) => { - setImageList(newImages); - const imageUrls = newImages.map((img) => img.url).join(","); - setValue("imageUrl", imageUrls); - }, - [setValue] - ); - const onSubmit: SubmitHandler = async (data) => { try { - if (!data.title) { - toast.error("제목을 입력하세요."); + if (!data.title || !data.content) { + toast.error("제목과 내용을 입력하세요."); return; } - if (!data.content) { - toast.error("내용을 입력하세요."); + if (isUploading) { + toast.error("이미지 업로드가 완료될 때까지 기다려주세요."); return; } const postData: PostSchema = { title: data.title, content: data.content, - imageUrl: imageList.map((img) => img.url).join(","), + imageUrl: data.imageUrl, }; editPost(postData, { @@ -154,108 +132,160 @@ export default function EditTalk({ params }: { params: { talkId: string } }) { if (!post) return null; return ( -
-
-
-
- 게시글 수정하기 -
-
- - -
-
-
-
- - ( - - )} - /> + +
+
+
+
게시글 수정
+
+ + +
+ +
+ + ( + + )} + /> +
-
- - ( -