├── src
├── stores
│ └── .gitkeep
├── components
│ ├── register
│ │ ├── Attraction
│ │ │ ├── index.tsx
│ │ │ ├── AttractionLand.tsx
│ │ │ └── AttractionJeju.tsx
│ │ ├── Button.tsx
│ │ ├── PlaceSearchBox.tsx
│ │ ├── SelectHomeImage
│ │ │ ├── UploadImage.tsx
│ │ │ ├── index.tsx
│ │ │ └── SelectImage.tsx
│ │ ├── PeopleInformation.tsx
│ │ ├── CohabitInformation.tsx
│ │ ├── LinkShare.tsx
│ │ ├── HouseType.tsx
│ │ ├── HomePrecautions.tsx
│ │ ├── PlaceInputContainer.tsx
│ │ ├── Counter.tsx
│ │ ├── HomeInformation.tsx
│ │ └── BottomSheet.tsx
│ ├── common
│ │ ├── ImageDiv.tsx
│ │ ├── SEO.tsx
│ │ ├── CloseHeader.tsx
│ │ ├── DropZone.tsx
│ │ ├── CheckboxButton.tsx
│ │ └── Modal.tsx
│ └── notification
│ │ └── Notification.tsx
├── services
│ └── libs
│ │ └── api.ts
├── pages
│ ├── search
│ │ └── index.tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── notification
│ │ └── index.tsx
│ ├── index.tsx
│ ├── detail
│ │ ├── [location].tsx
│ │ └── info
│ │ │ └── [id].tsx
│ └── register
│ │ └── index.tsx
├── hooks
│ ├── useModal.tsx
│ └── useToast.tsx
└── styles
│ └── globalStyle.ts
├── public
├── favicon.ico
└── assets
│ ├── images
│ ├── img_main.png
│ ├── index.ts
│ ├── img_ocean_small.svg
│ ├── img_ocean.svg
│ ├── img_upload.svg
│ ├── img_road_small.svg
│ ├── img_road.svg
│ ├── img_farm_small.svg
│ ├── img_farm.svg
│ ├── img_swimming.svg
│ ├── img_swimming_small.svg
│ ├── img_exercise_small.svg
│ ├── img_exercise.svg
│ ├── img_culture_small.svg
│ ├── img_activity_small.svg
│ └── img_hot_place_small.svg
│ └── icons
│ ├── ic_line.svg
│ ├── ic_check_empty.svg
│ ├── ic_back.svg
│ ├── ic_minus_active.svg
│ ├── ic_minus_blue.svg
│ ├── ic_minus_gray.svg
│ ├── ic_plus_active.svg
│ ├── ic_plus_blue.svg
│ ├── ic_plus_gray.svg
│ ├── ic_close.svg
│ ├── ic_check_active.svg
│ ├── ic_close_bg.svg
│ ├── ic_detail_back.svg
│ ├── ic_like.svg
│ ├── ic_location.svg
│ ├── ic_mark.svg
│ ├── ic_location_colored.svg
│ ├── ic_calendar.svg
│ ├── index.ts
│ ├── ic_brand.svg
│ ├── ic_notice.svg
│ ├── ic_alert.svg
│ ├── ic_home.svg
│ └── ic_kakao.svg
├── .babelrc
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE
│ └── feature_request.md
├── .prettierrc
├── next.config.js
├── .gitignore
├── tsconfig.json
├── .eslintrc.json
├── package.json
└── README.md
/src/stores/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NiZipNaeZip/frontend/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/assets/images/img_main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NiZipNaeZip/frontend/HEAD/public/assets/images/img_main.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [["styled-components", { "ssr": true, "displayName": true, "preprocess": false }]]
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/register/Attraction/index.tsx:
--------------------------------------------------------------------------------
1 | function Attraction() {
2 | return
Attraction
;
3 | }
4 |
5 | export default Attraction;
6 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## 🚩 관련 이슈
2 | - close #issue_number
3 |
4 | ## 📌 PR Point
5 | - 무슨 이유로 어떻게 코드를 변경했는지
6 | - 어떤 부분에 리뷰어가 집중해야 하는지
7 |
8 | ## 📸 스크린샷
9 |
--------------------------------------------------------------------------------
/src/components/register/Attraction/AttractionLand.tsx:
--------------------------------------------------------------------------------
1 | function AttractionLand() {
2 | return AttractionLand
;
3 | }
4 |
5 | export default AttractionLand;
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSameLine": true,
3 | "printWidth": 120,
4 | "semi": true,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "tabWidth": 2
8 | }
9 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_line.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_check_empty.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/services/libs/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const client = axios.create({
4 | baseURL: 'https://jipyo.link:8081',
5 | headers: {
6 | 'Content-Type': 'application/json',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_back.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ['jipyo.link'],
5 | },
6 | reactStrictMode: true,
7 | swcMinify: true,
8 | };
9 |
10 | module.exports = nextConfig;
11 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_minus_active.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_minus_blue.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_minus_gray.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## ✨ Description
11 | 구현할 기능 작성
12 |
13 | ## ✅ To Do List
14 | - [ ] 작업 1
15 | - [ ] 작업 2
16 | - [ ] 작업 3
17 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_plus_active.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_plus_blue.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_plus_gray.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_close.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_check_active.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/pages/search/index.tsx:
--------------------------------------------------------------------------------
1 | import SEO from '@src/components/common/SEO';
2 | import PlaceSearchBox from '@src/components/register/PlaceSearchBox';
3 |
4 | function Search() {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | );
11 | }
12 |
13 | export default Search;
14 |
--------------------------------------------------------------------------------
/src/components/common/ImageDiv.tsx:
--------------------------------------------------------------------------------
1 | import Image, { ImageProps } from 'next/image';
2 |
3 | function ImageDiv(props: ImageProps) {
4 | const { alt = '', className, ...rest } = props;
5 |
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default ImageDiv;
14 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_close_bg.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { AppProps } from 'next/app';
2 | import GlobalStyle from '@src/styles/globalStyle';
3 | import { RecoilRoot } from 'recoil';
4 |
5 | function MyApp({ Component, pageProps }: AppProps) {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default MyApp;
15 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_detail_back.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/src/components/common/SEO.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 |
3 | interface SEOProps {
4 | title: string;
5 | }
6 |
7 | function SEO(props: SEOProps) {
8 | const { title } = props;
9 |
10 | return (
11 |
12 | {title}
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default SEO;
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@src/*": ["src/*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/common/CloseHeader.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { icClose } from 'public/assets/icons';
3 | import styled from 'styled-components';
4 | import ImageDiv from './ImageDiv';
5 |
6 | function CloseHeader() {
7 | const router = useRouter();
8 |
9 | return (
10 |
11 |
14 |
15 | );
16 | }
17 |
18 | export default CloseHeader;
19 |
20 | const StCloseHeader = styled.div`
21 | padding: 16px 20px 17px 20px;
22 | height: 60px;
23 |
24 | .close {
25 | width: 27px;
26 | height: 27px;
27 | cursor: pointer;
28 | }
29 | `;
30 |
--------------------------------------------------------------------------------
/public/assets/images/index.ts:
--------------------------------------------------------------------------------
1 | export { default as imgUpload } from './img_upload.svg';
2 | export { default as imgAdd } from './img_add.svg';
3 | export { default as imgOcean } from './img_ocean.svg';
4 | export { default as imgExercise } from './img_exercise.svg';
5 | export { default as imgFarm } from './img_farm.svg';
6 | export { default as imgRoad } from './img_road.svg';
7 | export { default as imgSwimming } from './img_swimming.svg';
8 | export { default as imgOceanSmall } from './img_ocean_small.svg';
9 | export { default as imgExerciseSmall } from './img_exercise_small.svg';
10 | export { default as imgFarmSmall } from './img_farm_small.svg';
11 | export { default as imgRoadSmall } from './img_road_small.svg';
12 | export { default as imgSwimmingSmall } from './img_swimming_small.svg';
13 | export { default as imgActivitySmall } from './img_activity_small.svg';
14 |
--------------------------------------------------------------------------------
/src/components/register/Button.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | interface IProps {
4 | name: string;
5 | handleClick: () => void;
6 | nextValid: boolean;
7 | }
8 | export default function Button(props: IProps) {
9 | const { name, handleClick, nextValid } = props;
10 | return (
11 |
12 | {name}
13 |
14 | );
15 | }
16 |
17 | const StButton = styled.button<{ disabled: boolean }>`
18 | width: 100%;
19 | max-width: 380px;
20 | height: 61px;
21 | padding: 17px 0;
22 | border-radius: 10px;
23 | margin-top: 8px;
24 | text-align: center;
25 | font-weight: 500;
26 | font-size: 17px;
27 | line-height: 27px;
28 | color: white;
29 | background-color: ${({ disabled }) => (disabled ? '#E9E9FF' : '#6765FF')};
30 | cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
31 | `;
32 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_like.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "parser": "@typescript-eslint/parser",
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true
11 | },
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": ["@typescript-eslint"],
16 | "extends": [
17 | "eslint:recommended",
18 | "plugin:prettier/recommended",
19 | "plugin:@typescript-eslint/recommended",
20 | "plugin:@next/next/recommended",
21 | "next/core-web-vitals",
22 | "prettier"
23 | ],
24 | "rules": {
25 | "prettier/prettier": ["error", { "endOfLine": "auto" }, { "usePrettierrc": true }],
26 | "react/react-in-jsx-scope": "off",
27 | "react/prop-types": "off",
28 | "react/display-name": "off",
29 | "no-unused-vars": "off",
30 | "@typescript-eslint/no-var-requires": 0,
31 | "@typescript-eslint/no-unused-vars": ["error"],
32 | "@typescript-eslint/explicit-module-boundary-types": "off"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@types/react-daum-postcode": "^1.6.1",
13 | "@types/react-slick": "^0.23.10",
14 | "axios": "^1.1.2",
15 | "next": "12.3.1",
16 | "react": "18.2.0",
17 | "react-daum-postcode": "^3.1.1",
18 | "react-dom": "18.2.0",
19 | "react-dropzone": "^14.2.3",
20 | "react-slick": "^0.29.0",
21 | "recoil": "^0.7.6",
22 | "slick-carousel": "^1.8.1",
23 | "styled-components": "^5.3.6",
24 | "styled-reset": "^4.4.2"
25 | },
26 | "devDependencies": {
27 | "@types/node": "18.8.4",
28 | "@types/react": "18.0.21",
29 | "@types/react-dom": "18.0.6",
30 | "@types/styled-components": "^5.1.26",
31 | "babel-plugin-styled-components": "^2.0.7",
32 | "eslint": "8.25.0",
33 | "eslint-config-next": "12.3.1",
34 | "typescript": "4.8.4"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/register/PlaceSearchBox.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import DaumPostcodeEmbed from 'react-daum-postcode';
3 | import styled from 'styled-components';
4 | import CloseHeader from '../common/CloseHeader';
5 |
6 | function PlaceSearchBox() {
7 | const router = useRouter();
8 | const handleComplete = (data: { zonecode: string; address: string }) => {
9 | router.push({
10 | pathname: 'register',
11 | query: { zoneCode: data.zonecode, address: data.address },
12 | });
13 | };
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 |
21 | >
22 | );
23 | }
24 |
25 | export default PlaceSearchBox;
26 |
27 | const StPlaceSearchBox = styled.div`
28 | display: flex;
29 | flex-direction: column;
30 | align-items: center;
31 | & > div {
32 | width: 100vw !important;
33 | max-width: 42rem;
34 | height: 49.4rem !important;
35 | min-height: calc(100vh - 60px);
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_location.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_mark.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_location_colored.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/register/SelectHomeImage/UploadImage.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react';
2 | import styled from 'styled-components';
3 | import DropZone from '../../common/DropZone';
4 |
5 | interface IProps {
6 | setFiles: Dispatch>;
7 | setImages: Dispatch>;
8 | }
9 | export default function UploadImage(props: IProps) {
10 | const { setFiles, setImages } = props;
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | 당신의 집을 가장 잘 표현한 사진을
18 | 첫번째로 골라주세요!
19 |
20 |
21 | );
22 | }
23 |
24 | const StDropZoneDiv = styled.div`
25 | width: 375px;
26 | height: 222px;
27 | margin: 0 auto;
28 | margin-top: 100px;
29 | background: url('/assets/images/img_add.svg');
30 | cursor: pointer;
31 | `;
32 |
33 | const StInformationDiv = styled.div`
34 | margin-top: 33px;
35 | font-size: 14px;
36 | font-weight: 400;
37 | line-height: 22px;
38 | text-align: center;
39 | `;
40 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_calendar.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/register/PeopleInformation.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react';
2 | import styled from 'styled-components';
3 | import HouseType from './HouseType';
4 | import Counter from './Counter';
5 | import CohabitInformation from './CohabitInformation';
6 |
7 | interface PeopleInformationProps {
8 | setNextValid: Dispatch>;
9 | }
10 |
11 | function PeopleInformation(props: PeopleInformationProps) {
12 | const { setNextValid } = props;
13 |
14 | return (
15 |
16 | 어떤 집인가요?
17 |
18 |
19 | 몇 명이 지낼 수 있나요?
20 |
21 |
22 |
23 | 다른 사람과
24 |
25 | 함께 지내는 집인가요?
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default PeopleInformation;
33 |
34 | const StLine = styled.div`
35 | max-width: 420px;
36 | width: 100%;
37 | background: #f9f9f9;
38 | height: 10px;
39 | `;
40 |
41 | const StTitle = styled.h2`
42 | padding: 0 20px;
43 | padding-top: 38px;
44 |
45 | &:last-child {
46 | margin-bottom: 30px;
47 | }
48 | `;
49 |
--------------------------------------------------------------------------------
/public/assets/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as icBack } from './ic_back.svg';
2 | export { default as icClose } from './ic_close.svg';
3 | export { default as icMinusGray } from './ic_minus_gray.svg';
4 | export { default as icMinusActive } from './ic_minus_active.svg';
5 | export { default as icPlusGray } from './ic_plus_gray.svg';
6 | export { default as icPlusActive } from './ic_plus_active.svg';
7 | export { default as icCheckEmpty } from './ic_check_empty.svg';
8 | export { default as icCheckActive } from './ic_check_active.svg';
9 | export { default as icCloseBg } from './ic_close_bg.svg';
10 | export { default as icAlert } from './ic_alert.svg';
11 | export { default as icLocation } from './ic_location.svg';
12 | export { default as icLocationColored } from './ic_location_colored.svg';
13 | export { default as icLine } from './ic_line.svg';
14 | export { default as icCalendar } from './ic_calendar.svg';
15 | export { default as icLike } from './ic_like.svg';
16 | export { default as icMark } from './ic_mark.svg';
17 | export { default as icKakao } from './ic_kakao.svg';
18 | export { default as icBrand } from './ic_brand.svg';
19 | export { default as icHome } from './ic_home.svg';
20 | export { default as icNotice } from './ic_notice.svg';
21 | export { default as icDetailBack } from './ic_detail_back.svg';
22 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 |
4 | export default class MyDocument extends Document {
5 | static async getInitialProps(ctx: DocumentContext) {
6 | const sheet = new ServerStyleSheet();
7 | const originalRenderPage = ctx.renderPage;
8 | try {
9 | ctx.renderPage = () =>
10 | originalRenderPage({
11 | enhanceApp: (App) => (props) => sheet.collectStyles(),
12 | });
13 | const initialProps = await Document.getInitialProps(ctx);
14 | return {
15 | ...initialProps,
16 | styles: (
17 | <>
18 | {' '}
19 | {initialProps.styles} {sheet.getStyleElement()}{' '}
20 | >
21 | ),
22 | };
23 | } finally {
24 | sheet.seal();
25 | }
26 | }
27 |
28 | render() {
29 | return (
30 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_brand.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/components/common/DropZone.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react';
2 | import { ReactElement, useCallback } from 'react';
3 | import { useDropzone } from 'react-dropzone';
4 |
5 | interface IProps {
6 | setFiles: Dispatch>;
7 | setImages: Dispatch>;
8 | children: React.ReactNode;
9 | }
10 | const DropZone: React.FC = ({ setFiles, setImages, children }) => {
11 | const onDrop = useCallback((acceptedFiles: Blob[]) => {
12 | acceptedFiles.forEach((file: Blob) => {
13 | const reader = new FileReader();
14 | setFiles((prev) => [...prev, file]);
15 | const bloburl = URL.createObjectURL(file);
16 | setImages((prev) => [...prev, bloburl]);
17 | reader.onabort = () => console.log('file reading was aborted');
18 | reader.onerror = () => console.log('file reading has failed');
19 | reader.onload = () => {
20 | // Do whatever you want with the file contents
21 | const binaryStr = reader.result;
22 | };
23 | reader.readAsArrayBuffer(file);
24 | });
25 | }, []);
26 | const { getRootProps, getInputProps } = useDropzone({ onDrop });
27 | return (
28 |
29 |
30 | {children}
31 |
32 | );
33 | };
34 |
35 | export default DropZone;
36 |
--------------------------------------------------------------------------------
/src/components/register/SelectHomeImage/index.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react';
2 | import styled from 'styled-components';
3 | import SelectImage from './SelectImage';
4 | import UploadImage from './UploadImage';
5 |
6 | interface IProps {
7 | setFiles: Dispatch>;
8 | setImages: Dispatch>;
9 | setRepresentImg: Dispatch>;
10 | setNextValid: Dispatch>;
11 | images: string[];
12 | representImg: string | null;
13 | }
14 |
15 | export default function SelectHomeImage(props: IProps) {
16 | const { setFiles, setImages, images, representImg, setRepresentImg, setNextValid } = props;
17 | return (
18 |
19 |
20 | 내 집은
21 | 이렇게 생겼어요!
22 |
23 | {images.length === 0 ? (
24 |
25 | ) : (
26 |
32 | )}
33 |
34 | );
35 | }
36 |
37 | const StMainDiv = styled.div`
38 | width: 100%;
39 | `;
40 |
41 | const StHeaderDiv = styled.div`
42 | margin-top: 40px;
43 | padding: 0 20px;
44 | `;
45 |
--------------------------------------------------------------------------------
/src/hooks/useModal.tsx:
--------------------------------------------------------------------------------
1 | import Modal from '@src/components/common/Modal';
2 | import { useState } from 'react';
3 |
4 | interface IProps {
5 | isConfirm: boolean;
6 | title: string;
7 | content: string;
8 | rightComment: string;
9 | leftComment: string;
10 | handleRightButton?: () => void;
11 | handleLeftButton: () => void;
12 | }
13 |
14 | export default function useModal(props: IProps) {
15 | const { isConfirm, title, rightComment, content, leftComment, handleRightButton, handleLeftButton } = props;
16 | const [isOpen, setIsOpen] = useState(false);
17 | const openModal = () => setIsOpen(true);
18 | const closeModal = () => setIsOpen(false);
19 | const handleRightButtonClick = () => {
20 | if (isConfirm) {
21 | setIsOpen(false);
22 | return;
23 | }
24 | if (handleRightButton) handleRightButton();
25 | setIsOpen(false);
26 | };
27 | const modal = () => (
28 | {
37 | handleLeftButton();
38 | setIsOpen(false);
39 | }}
40 | handleRightButton={handleRightButtonClick}
41 | />
42 | );
43 | return {
44 | openModal,
45 | Modal: modal,
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_notice.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/register/CohabitInformation.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import styled from 'styled-components';
3 | import CheckboxButton from '../common/CheckboxButton';
4 |
5 | function CohabitInformation() {
6 | const [checkedButton, setCheckedButton] = useState('집 전체');
7 |
8 | return (
9 |
10 | setCheckedButton('집 전체')}
12 | isChecked={checkedButton === '집 전체'}
13 | option="집 전체"
14 | description="모든 공간을 단독으로 사용하고 있어요."
15 | />
16 | setCheckedButton('1인실')}
18 | isChecked={checkedButton === '1인실'}
19 | option="1인실"
20 | description="침실은 단독으로 사용하지만, 욕실 및 주방 등 다른 사람과 공유하는 공간이 있어요."
21 | />
22 | setCheckedButton('다인실')}
24 | isChecked={checkedButton === '다인실'}
25 | option="다인실"
26 | description="침실을 포함한 모든 공간을 다른 사람과 공유하고 있어요."
27 | />
28 |
29 | );
30 | }
31 |
32 | export default CohabitInformation;
33 |
34 | const StCohabitInformation = styled.div`
35 | margin: 30px 20px 40px 20px;
36 |
37 | & > button {
38 | display: flex;
39 | align-items: center;
40 | gap: 15px;
41 | width: 100%;
42 | min-width: fit-content;
43 | padding: 10px 0;
44 |
45 | &:not(:last-child) {
46 | margin-bottom: 15px;
47 | }
48 | }
49 | `;
50 |
--------------------------------------------------------------------------------
/src/styles/globalStyle.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 | import reset from 'styled-reset';
3 |
4 | const GlobalStyle = createGlobalStyle`
5 | ${reset};
6 |
7 | html,
8 | body {
9 | width: 100%;
10 | height: 100%;
11 | }
12 |
13 | h2 {
14 | font-size: 21px;
15 | font-weight: 700;
16 | line-height: 28px;
17 | letter-spacing: -0.004em;
18 | text-align: left;
19 | }
20 |
21 | h5 {
22 | font-size: 21px;
23 | font-weight: 700;
24 | line-height: 34px;
25 | text-align: left;
26 | }
27 |
28 | #__next {
29 | display: flex;
30 | flex-direction: column;
31 | max-width: 42rem;
32 | min-height: 100vh;
33 | margin: 0 auto;
34 | }
35 |
36 | html {
37 | font-size: 62.5%;
38 | }
39 |
40 | * {
41 | box-sizing: border-box;
42 | }
43 |
44 | body, button, input, textarea {
45 | font-family: 'Noto Sans KR', sans-serif;
46 | }
47 |
48 | textarea {
49 | border: none;
50 | outline: none;
51 | resize: none;
52 | }
53 |
54 | button {
55 | cursor: pointer;
56 | border: none;
57 | outline: none;
58 | background-color: transparent;
59 | -webkit-tap-highlight-color : transparent;
60 | padding: 0;
61 | }
62 |
63 | input {
64 | outline: none;
65 | border: none;
66 | }
67 |
68 | a, a:visited {
69 | text-decoration: none;
70 | color: black;
71 | }
72 | `;
73 |
74 | export default GlobalStyle;
75 |
--------------------------------------------------------------------------------
/src/components/register/LinkShare.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 | import ImageDiv from '../common/ImageDiv';
4 | import { icKakao } from 'public/assets/icons';
5 |
6 | interface LinkShareProps {
7 | setNextValid: Dispatch>;
8 | }
9 |
10 | function LinkShare(props: LinkShareProps) {
11 | const { setNextValid } = props;
12 | const [link, setLink] = useState('');
13 | const handleChange = (e: React.ChangeEvent) => {
14 | setLink(e.target.value);
15 | };
16 |
17 | useEffect(() => {
18 | setNextValid(link ? true : false);
19 | }, [link]);
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | 소통할 채팅방의 링크를
27 |
28 | 공유해 주세요
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default LinkShare;
36 |
37 | const StLinkShare = styled.div`
38 | padding: 0 20px;
39 |
40 | input {
41 | width: 100%;
42 | height: 46px;
43 | margin-top: 30px;
44 | font-size: 12px;
45 | line-height: 160.3%;
46 | border-bottom: 1px solid #e1e1e1;
47 | }
48 | `;
49 |
50 | const StHeader = styled.div`
51 | font-weight: 700;
52 | font-size: 21px;
53 | line-height: 160.3%;
54 | margin-top: 40px;
55 |
56 | & > p {
57 | display: flex;
58 | }
59 | `;
60 |
--------------------------------------------------------------------------------
/public/assets/images/img_ocean_small.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/components/register/SelectHomeImage/SelectImage.tsx:
--------------------------------------------------------------------------------
1 | import ImageDiv from '@src/components/common/ImageDiv';
2 | import { Dispatch, SetStateAction } from 'react';
3 | import styled from 'styled-components';
4 |
5 | interface IProps {
6 | images: string[];
7 | representImg: string | null;
8 | setRepresentImg: Dispatch>;
9 | setNextValid: Dispatch>;
10 | }
11 |
12 | export default function SelectImage(props: IProps) {
13 | const { images, representImg, setRepresentImg, setNextValid } = props;
14 | setNextValid(true);
15 | return (
16 |
17 |
18 |
19 | {images.map((image) => (
20 | setRepresentImg(image)}
26 | />
27 | ))}
28 |
29 |
30 | );
31 | }
32 |
33 | const StSelectImage = styled.div`
34 | margin-top: 40px;
35 | padding: 0 20px;
36 |
37 | .representative {
38 | position: relative;
39 | width: 100%;
40 | max-width: 420px;
41 | height: 312px;
42 | margin-bottom: 16px;
43 | }
44 |
45 | img {
46 | border-radius: 8px;
47 | object-fit: cover;
48 | }
49 | `;
50 |
51 | const StImageContainer = styled.div`
52 | display: flex;
53 | flex-wrap: wrap;
54 | gap: 14px;
55 |
56 | .selected-image {
57 | position: relative;
58 | width: calc(calc(100% - 28px) / 3);
59 | height: 108px;
60 | cursor: pointer;
61 | }
62 | `;
63 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_alert.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/images/img_ocean.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/public/assets/images/img_upload.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_home.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/register/HouseType.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | function HouseType() {
4 | return (
5 |
6 |
7 |
건물 유형
8 |
14 |
15 |
16 |
방 개수
17 |
23 |
24 |
25 |
전체 평수
26 |
32 |
33 |
34 | );
35 | }
36 |
37 | export default HouseType;
38 |
39 | const StHouseType = styled.div`
40 | display: flex;
41 | margin: 30px 20px 40px 20px;
42 |
43 | & > div {
44 | width: calc(100% / 3);
45 | height: 62px;
46 | padding: 9px 10px 10px 12px;
47 | background: #f3f7fb;
48 |
49 | div {
50 | font-size: 12px;
51 | line-height: 160.3%;
52 | color: #9190cf;
53 | }
54 | }
55 |
56 | & > div:first-child {
57 | border-top-left-radius: 10px;
58 | border-bottom-left-radius: 10px;
59 | }
60 |
61 | & > div:last-child {
62 | border-top-right-radius: 10px;
63 | border-bottom-right-radius: 10px;
64 | }
65 |
66 | & > div:not(:last-child) {
67 | border-right: 1px solid #9190cf;
68 | }
69 |
70 | select {
71 | width: 100%;
72 | margin-top: 5px;
73 | background-color: transparent;
74 | font-size: 14px;
75 | line-height: 22px;
76 | border: 0;
77 | outline: 0;
78 | }
79 | `;
80 |
--------------------------------------------------------------------------------
/src/components/register/HomePrecautions.tsx:
--------------------------------------------------------------------------------
1 | import { icAlert } from 'public/assets/icons';
2 | import { Dispatch, SetStateAction, useEffect, useState } from 'react';
3 | import styled from 'styled-components';
4 | import ImageDiv from '../common/ImageDiv';
5 |
6 | interface IProps {
7 | setNextValid: Dispatch>;
8 | }
9 | export default function HomePrecautions(props: IProps) {
10 | const { setNextValid } = props;
11 | const [precautions, setPrecautions] = useState('');
12 | setNextValid(true);
13 | return (
14 |
15 |
16 |
17 | 이런 활동은 주의해 주세요!
18 |
19 |
20 |
27 |
28 | );
29 | }
30 |
31 | const StHeaderDiv = styled.div`
32 | display: flex;
33 | margin-top: 40px;
34 |
35 | .alert {
36 | width: 34px;
37 | height: 34px;
38 | }
39 | `;
40 |
41 | const StHomePrecautions = styled.div`
42 | display: flex;
43 | flex-direction: column;
44 | text-align: center;
45 | padding: 0 20px;
46 |
47 | & > span {
48 | display: block;
49 | justify-self: end;
50 | }
51 | `;
52 |
53 | const StRecommendInfo = styled.div`
54 | display: flex;
55 | flex-direction: column;
56 | height: 302px;
57 | margin-top: 30px;
58 | background-color: #f9f9f9;
59 | border-radius: 10px;
60 | padding: 20px;
61 |
62 | textarea {
63 | height: 100%;
64 | background-color: #f9f9f9;
65 | }
66 |
67 | span {
68 | height: 19.24px;
69 | line-height: 19.24px;
70 | font-size: 12px;
71 | font-weight: 400;
72 | color: #a3a3a3;
73 | text-align: right;
74 | }
75 | `;
76 |
--------------------------------------------------------------------------------
/src/components/common/CheckboxButton.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { icCheckActive, icCheckEmpty } from 'public/assets/icons';
3 | import ImageDiv from './ImageDiv';
4 |
5 | interface CheckboxButtonProps {
6 | onClick: () => void;
7 | isChecked: boolean;
8 | option: string;
9 | description: string;
10 | image?: string;
11 | }
12 |
13 | function CheckboxButton(props: CheckboxButtonProps) {
14 | const { onClick, isChecked, option, description, image } = props;
15 |
16 | return (
17 |
18 |
19 | {image && }
20 |
21 | {option}
22 | {description}
23 |
24 |
25 | );
26 | }
27 |
28 | export default CheckboxButton;
29 |
30 | const StCheckboxButton = styled.button<{ isChecked: boolean }>`
31 | display: flex;
32 | align-items: center;
33 | border-radius: 10px;
34 |
35 | .check {
36 | margin-left: 24px;
37 | width: 20px;
38 | height: 20px;
39 | }
40 |
41 | .circle {
42 | margin: 12px 12px 12px 14px;
43 | width: 54px;
44 | height: 54px;
45 | }
46 |
47 | ${({ isChecked }) =>
48 | isChecked &&
49 | `box-shadow: 0px 7px 18px rgba(0, 0, 0, 0.04);
50 | border: 1px solid #eef3f9;`}
51 | `;
52 |
53 | const StTextBox = styled.div<{ isChecked: boolean }>`
54 | width: 100%;
55 | flex: 1;
56 | text-align: left;
57 |
58 | & > div:first-child {
59 | font-weight: 500;
60 | font-size: 16px;
61 | line-height: 160.3%;
62 | color: ${({ isChecked }) => isChecked && `#6765ff`};
63 | }
64 |
65 | & > div:last-child {
66 | margin-top: 5px;
67 | font-size: 12px;
68 | line-height: 160.3%;
69 | color: #a3a3a3;
70 | word-break: keep-all;
71 | }
72 | `;
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ##
니집내집
2 | > 숙박비 없는 여행의 시작! 우리, 집 바꿔 살래요?
3 |
4 | 
5 |
6 | ### 1. 프로젝트 소개
7 | 니집내집은 일정 기간 집을 맞바꿔 살아보는 **집 교환(Home Exchange) 플랫폼**입니다.
8 |
9 | - background : 누구나 한 번쯤은 꿈꿔본 제주 한 달 살기
10 | - problem : 제주 한 달 살기의 가장 큰 장벽은 숙박비
11 | - solution : 집을 바꿔 사는 여행
12 | - target : 장기 여행 가고는 싶은데 주머니 사정이 팍팍한 2030
13 | - vision : 누구나 도시와 지방을 넘나들며 듀얼 라이프를 누릴 수 있는 세상
14 |
15 | ### 2. 프로젝트 진행 배경
16 | #### [kakao x goorm] 2nd 9oormthon
17 | 카카오와 구름이 주관한 제2회 [구름톤](https://9oormthon.goorm.io/)에 참여해 진행한 프로젝트입니다.
18 | - 개발 기간 : 2022.10.12 - 2022.10.14
19 | - 해커톤 주제 : `#제주`, `#클라우드`, `#관광`
20 | - 해커톤 결과 : **최우수상 수상🏆**
21 |
22 | ### 3. 주요 기능
23 | 
24 | 
25 | 
26 | 
27 | 
28 | 
29 | 
30 |
31 |
32 | ### 4. 기술 스택
33 | 
34 | 
35 | 
36 |
--------------------------------------------------------------------------------
/src/pages/notification/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 | import { icBack } from 'public/assets/icons';
4 | import ImageDiv from '@src/components/common/ImageDiv';
5 | import { useRouter } from 'next/router';
6 | import Notification from '@src/components/notification/Notification';
7 | import { client } from '@src/services/libs/api';
8 | import SEO from '@src/components/common/SEO';
9 |
10 | export default function NotificationPage() {
11 | const router = useRouter();
12 | const [noticeList, setNoticeList] = useState([]);
13 | const handleClickPrevious = () => {
14 | router.push('/');
15 | };
16 | useEffect(() => {
17 | (async () => {
18 | const { data } = await client.get(`/user/1/notice`);
19 | setNoticeList(data);
20 | })();
21 | }, []);
22 |
23 | return (
24 | <>
25 |
26 |
27 |
28 |
29 | 알림
30 |
31 |
32 | {noticeList.map(({ alarm_id, alarmStatus, viewMyNoticeImageResDTO, address }) => (
33 |
43 | ))}
44 |
45 |
46 | >
47 | );
48 | }
49 |
50 | const StMainContainer = styled.div`
51 | padding: 0 20px;
52 | `;
53 |
54 | const StHeader = styled.div`
55 | display: flex;
56 | align-items: center;
57 | height: 60px;
58 |
59 | h5 {
60 | margin: 0 auto;
61 | }
62 |
63 | .back {
64 | cursor: pointer;
65 | width: 27px;
66 | height: 27px;
67 | }
68 | `;
69 |
--------------------------------------------------------------------------------
/src/components/register/PlaceInputContainer.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useRouter } from 'next/router';
3 | import { useEffect, useState } from 'react';
4 | import styled from 'styled-components';
5 | import { Dispatch, SetStateAction } from 'react';
6 |
7 | interface PlaceInputContainerProps {
8 | setNextValid: Dispatch>;
9 | }
10 |
11 | function PlaceInputContainer(props: PlaceInputContainerProps) {
12 | const { setNextValid } = props;
13 | const [detail, setDetail] = useState('');
14 | const router = useRouter();
15 | const { zoneCode, address } = router.query;
16 | const handleChange = (e: React.ChangeEvent) => {
17 | setDetail(e.target.value);
18 | };
19 |
20 | useEffect(() => {
21 | const passCondition = zoneCode && address && detail;
22 | setNextValid(passCondition ? true : false);
23 | }, [detail]);
24 |
25 | return (
26 |
27 | 어디에 살고 계신가요?
28 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | export default PlaceInputContainer;
41 |
42 | const StPlaceInputContainer = styled.div`
43 | display: flex;
44 | flex-direction: column;
45 | gap: 14px;
46 | padding: 0 20px;
47 | justify-content: center;
48 |
49 | h1 {
50 | font-weight: 700;
51 | font-size: 21px;
52 | line-height: 160.3%;
53 | margin-bottom: 26px;
54 | }
55 |
56 | div {
57 | display: flex;
58 | gap: 19px;
59 |
60 | a {
61 | height: 46px;
62 | background: #e9e9ff;
63 | border-radius: 10px;
64 | color: #6765ff;
65 | padding: 12px 20px 12px 19px;
66 | font-size: 14px;
67 | line-height: 22px;
68 | }
69 | }
70 |
71 | input {
72 | border-bottom: 1px solid #e1e1e1;
73 | height: 46px;
74 | }
75 |
76 | input:first-child {
77 | flex: 1;
78 | }
79 |
80 | input:not(:last-child) {
81 | cursor: default;
82 | }
83 | `;
84 |
--------------------------------------------------------------------------------
/src/components/register/Counter.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 | import ImageDiv from '../common/ImageDiv';
4 | import { icMinusActive, icMinusGray, icPlusActive, icPlusGray } from 'public/assets/icons';
5 |
6 | interface IProps {
7 | setNextValid: Dispatch>;
8 | }
9 |
10 | function Counter(props: IProps) {
11 | const { setNextValid } = props;
12 | const [count, setCount] = useState(0);
13 | const [isMinusAdjustable, setIsMinusAdjustable] = useState(false);
14 | const [isPlusAdjustable, setIsPlusAdjustable] = useState(true);
15 |
16 | const handlePlusClick = () => {
17 | setCount((prev) => prev + 1);
18 | };
19 |
20 | const handleMinusClick = () => {
21 | setCount((prev) => prev - 1);
22 | };
23 |
24 | useEffect(() => {
25 | if (count > 3) {
26 | setIsPlusAdjustable(false);
27 | setIsMinusAdjustable(true);
28 | } else if (count === 0) {
29 | setIsPlusAdjustable(true);
30 | setIsMinusAdjustable(false);
31 | setNextValid(false);
32 | } else {
33 | setIsPlusAdjustable(true);
34 | setIsMinusAdjustable(true);
35 | setNextValid(true);
36 | }
37 | }, [count]);
38 |
39 | return (
40 |
41 |
44 | {count > 3 ? `3명 이상` : `${count}명`}
45 |
48 |
49 | );
50 | }
51 |
52 | export default Counter;
53 |
54 | const StCounter = styled.div`
55 | display: flex;
56 | align-items: center;
57 | justify-content: center;
58 | gap: 38px;
59 | margin-top: 30px;
60 | margin-bottom: 40px;
61 |
62 | div {
63 | font-weight: 500;
64 | font-size: 18px;
65 | line-height: 160.3%;
66 | }
67 |
68 | button:disabled {
69 | cursor: not-allowed;
70 | }
71 |
72 | .button {
73 | width: 26px;
74 | height: 26px;
75 | }
76 | `;
77 |
--------------------------------------------------------------------------------
/src/hooks/useToast.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react';
2 | import styled from 'styled-components';
3 |
4 | export default function useToast(content: string) {
5 | const [animationName, setAnimationName] = useState('slide-in');
6 | const [isOpen, setIsOpen] = useState(false);
7 | const timeoutId = useRef>(null);
8 | const removeToast = () => {
9 | if (timeoutId.current) clearTimeout(timeoutId.current);
10 | setAnimationName('slide-out');
11 | setTimeout(() => {
12 | setIsOpen(false);
13 | setAnimationName('slide-in');
14 | }, 250);
15 | };
16 | const openToast = () => {
17 | if (timeoutId.current) clearTimeout(timeoutId.current);
18 | if (isOpen) {
19 | removeToast();
20 | setTimeout(() => setIsOpen(true), 260);
21 | } else {
22 | setIsOpen(true);
23 | }
24 | timeoutId.current = setTimeout(removeToast, 3000);
25 | };
26 | const ToastModal = () => (
27 |
28 |
29 | {content}
30 |
31 |
32 | );
33 | return {
34 | openToast,
35 | ToastModal,
36 | };
37 | }
38 |
39 | const StToastContainer = styled.div<{ isOpen: boolean }>`
40 | z-index: 999;
41 | display: ${({ isOpen }) => (isOpen ? 'block' : 'none')};
42 | position: fixed;
43 | top: 76px;
44 | width: calc(100% - 40px);
45 | max-width: 380px;
46 | margin-left: 20px;
47 | height: 84px;
48 | border-radius: 10px;
49 | padding: 20px;
50 | background-color: black;
51 | color: white;
52 | //styleName: regular/p;
53 | font-size: 14px;
54 | font-weight: 400;
55 | line-height: 22px;
56 | text-align: left;
57 | `;
58 |
59 | const StAnimationWrapper = styled.div`
60 | .slide-in {
61 | animation-duration: 0.35s;
62 | animation-name: slidein;
63 | }
64 | .slide-out {
65 | animation-duration: 0.35s;
66 | animation-name: slideout;
67 | }
68 |
69 | @keyframes slidein {
70 | from {
71 | top: -84px;
72 | }
73 | to {
74 | top: 76px;
75 | }
76 | }
77 | @keyframes slideout {
78 | from {
79 | top: 76px;
80 | }
81 | to {
82 | top: -84px;
83 | }
84 | }
85 | `;
86 |
--------------------------------------------------------------------------------
/public/assets/images/img_road_small.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/src/components/register/HomeInformation.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 |
4 | interface IProps {
5 | setNextValid: Dispatch>;
6 | }
7 | export default function HomeInformation(props: IProps) {
8 | const { setNextValid } = props;
9 | const [oneWordInfo, setOneWordInfo] = useState('');
10 | const [recommendInfo, setRecommendInfo] = useState('');
11 |
12 | useEffect(() => {
13 | setNextValid(oneWordInfo !== '' && recommendInfo !== '');
14 | }, [oneWordInfo, recommendInfo]);
15 | return (
16 |
17 |
18 | 당신의 집을 한마디로 표현한다면?
19 |
20 |
21 | setOneWordInfo(e.target.value)} />
22 | 최대 30자
23 |
24 |
25 |
26 |
27 | 이런 분께 추천합니다.
28 |
29 |
30 |
37 |
38 |
39 | );
40 | }
41 |
42 | const StOneWordHeader = styled.div`
43 | margin-top: 40px;
44 | padding: 0 20px;
45 | `;
46 |
47 | const StRecommendHeader = styled.div`
48 | margin-top: 38px;
49 | `;
50 |
51 | const StBrDiv = styled.div`
52 | height: 10px;
53 | background-color: #f9f9f9;
54 | `;
55 |
56 | const StOneWordInfo = styled.div`
57 | display: flex;
58 | margin: 30px 20px 40px 20px;
59 | height: 46px;
60 | border-bottom: 1px solid #a3a3a3;
61 | box-sizing: content-box;
62 | input {
63 | width: calc(100% - 63px);
64 | height: 46px;
65 | margin-right: 13px;
66 | border: none;
67 | }
68 | span {
69 | width: 50px;
70 | height: 46px;
71 | line-height: 46px;
72 | font-family: Noto Sans KR;
73 | font-size: 12px;
74 | font-weight: 400;
75 | letter-spacing: 0em;
76 | text-align: right;
77 | color: #a3a3a3;
78 | }
79 | `;
80 |
81 | const StRecommend = styled.div`
82 | padding: 0 20px;
83 | `;
84 |
85 | const StRecommendInfo = styled.div`
86 | display: flex;
87 | flex-direction: column;
88 | height: 302px;
89 | margin-top: 30px;
90 | background-color: #f9f9f9;
91 | border-radius: 10px;
92 | padding: 20px;
93 |
94 | textarea {
95 | height: 100%;
96 | background-color: #f9f9f9;
97 | }
98 |
99 | span {
100 | height: 19.24px;
101 | line-height: 19.24px;
102 | font-size: 12px;
103 | font-weight: 400;
104 | color: #a3a3a3;
105 | text-align: right;
106 | }
107 | `;
108 |
--------------------------------------------------------------------------------
/public/assets/images/img_road.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/src/components/common/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { icClose } from 'public/assets/icons';
2 | import styled from 'styled-components';
3 | import ImageDiv from './ImageDiv';
4 |
5 | interface IProps {
6 | isConfirm: boolean;
7 | isOpen: boolean;
8 | title: string;
9 | content: string;
10 | leftComment: string;
11 | rightComment: string;
12 | closeModal: () => void;
13 | handleRightButton: () => void;
14 | handleLeftButton: () => void;
15 | }
16 |
17 | export default function Modal(props: IProps) {
18 | const {
19 | isOpen,
20 | isConfirm,
21 | title,
22 | content,
23 | leftComment,
24 | rightComment,
25 | handleRightButton,
26 | handleLeftButton,
27 | closeModal,
28 | } = props;
29 | return (
30 |
31 | e.stopPropagation()}>
32 |
33 | {title}
34 | {content}
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | const StModalBackground = styled.div<{ isOpen: boolean }>`
45 | display: ${({ isOpen }) => (isOpen ? 'block' : 'none')};
46 | position: fixed;
47 | width: 100%;
48 | max-width: 420px;
49 | height: 100%;
50 | z-index: 1;
51 | overflow: auto;
52 | background-color: rgba(0, 0, 0, 0.4);
53 | `;
54 |
55 | const StModalContent = styled.div`
56 | z-index: 2;
57 | position: fixed;
58 | top: 50%;
59 | left: 50%;
60 | transform: translate(-50%, -50%);
61 | border-radius: 15px;
62 | background-color: white;
63 | text-align: center;
64 | max-width: 380px;
65 | width: calc(100% - 40px);
66 | padding: 16px 20px;
67 |
68 | h5 {
69 | font-size: 18px;
70 | font-weight: 500;
71 | line-height: 29px;
72 | letter-spacing: 0em;
73 | text-align: center;
74 | margin-bottom: 10px;
75 | }
76 |
77 | pre {
78 | font-size: 14px;
79 | font-weight: 400;
80 | line-height: 26px;
81 | letter-spacing: 0em;
82 | text-align: center;
83 | }
84 |
85 | .close {
86 | width: 27px;
87 | height: 27px;
88 | cursor: pointer;
89 | margin-bottom: 35px;
90 | margin-left: auto;
91 | }
92 | `;
93 |
94 | const StButtonContainer = styled.div<{ isConfirm: boolean }>`
95 | display: flex;
96 | justify-content: space-between;
97 | gap: 14px;
98 | margin-top: 25px;
99 | margin-bottom: 2px;
100 |
101 | button {
102 | width: 100%;
103 | height: 50px;
104 | border-radius: 10px;
105 | font-weight: 500;
106 | }
107 |
108 | button:first-child {
109 | background-color: ${({ isConfirm }) => (isConfirm ? '#ef4040' : '#FFB84D')};
110 | color: ${({ isConfirm }) => (isConfirm ? 'white' : 'black')};
111 | }
112 |
113 | button:last-child {
114 | background-color: ${({ isConfirm }) => (isConfirm ? '#ececec' : '#13CC89')};
115 | }
116 | `;
117 |
--------------------------------------------------------------------------------
/src/components/register/Attraction/AttractionJeju.tsx:
--------------------------------------------------------------------------------
1 | import CheckboxButton from '@src/components/common/CheckboxButton';
2 | import {
3 | imgActivitySmall,
4 | imgExerciseSmall,
5 | imgFarmSmall,
6 | imgOceanSmall,
7 | imgRoadSmall,
8 | imgSwimmingSmall,
9 | } from 'public/assets/images';
10 | import { Dispatch, SetStateAction, useEffect, useState } from 'react';
11 | import styled from 'styled-components';
12 |
13 | interface AttractionJejuProps {
14 | setNextValid: Dispatch>;
15 | }
16 |
17 | function AttractionJeju(props: AttractionJejuProps) {
18 | const { setNextValid } = props;
19 | const [placeList, setPlaceList] = useState([]);
20 | const [rentalList, setRentalList] = useState([]);
21 | setNextValid(true);
22 |
23 | return (
24 |
25 | 집 근처에서 어떻게 노나요?
26 |
27 | setPlaceList([...placeList, '해변'])}
30 | isChecked={placeList.includes('해변')}
31 | option="해변"
32 | description="집 근처에 바닷가가 있어요."
33 | />
34 | setPlaceList([...placeList, '올레길'])}
37 | isChecked={placeList.includes('올레길')}
38 | option="올레길"
39 | description="집 근처에 산책 가능한 올레길이 있어요."
40 | />
41 | setPlaceList([...placeList, '감귤따기'])}
44 | isChecked={placeList.includes('감귤따기')}
45 | option="감귤따기"
46 | description="집 근처 감귤농장에서 체험할 수 있어요."
47 | />
48 | setPlaceList([...placeList, '액티비티 체험'])}
51 | isChecked={placeList.includes('액티비티 체험')}
52 | option="액티비티 체험"
53 | description="해녀 체험, 승마, 카약 등이 가능해요."
54 | />
55 |
56 |
57 | 빌려드려요
58 |
59 | setRentalList([...rentalList, '운동 기구'])}
62 | isChecked={rentalList.includes('운동 기구')}
63 | option="운동 기구"
64 | description="요가 매트, 자전거, 산악 바이크 등"
65 | />
66 | setRentalList([...rentalList, '물놀이 용품'])}
69 | isChecked={rentalList.includes('물놀이 용품')}
70 | option="물놀이 용품"
71 | description="서핑 보드, 낚시 용품, 오리발 등"
72 | />
73 |
74 |
75 | );
76 | }
77 |
78 | export default AttractionJeju;
79 |
80 | const StAttractionJeju = styled.div`
81 | margin-bottom: 40px;
82 | `;
83 |
84 | const StTitle = styled.h2`
85 | padding: 0 20px;
86 | margin-top: 40px;
87 | margin-bottom: 30px;
88 | `;
89 |
90 | const StLine = styled.div`
91 | max-width: 420px;
92 | width: 100%;
93 | background: #f9f9f9;
94 | height: 10px;
95 | margin-top: 56px;
96 | margin-bottom: 38px;
97 | `;
98 |
99 | const StOptionList = styled.div`
100 | display: flex;
101 | flex-direction: column;
102 | gap: 15px;
103 | padding: 0 20px;
104 | `;
105 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import ImageDiv from '@src/components/common/ImageDiv';
2 | import SEO from '@src/components/common/SEO';
3 | import useModal from '@src/hooks/useModal';
4 | import useToast from '@src/hooks/useToast';
5 | import Link from 'next/link';
6 | import { useRouter } from 'next/router';
7 | import { icBrand, icHome, icNotice } from 'public/assets/icons';
8 | import { useEffect } from 'react';
9 | import styled from 'styled-components';
10 |
11 | function Home() {
12 | const router = useRouter();
13 | const query = router.query;
14 | const { openModal, Modal }: any = useModal({
15 | isConfirm: false,
16 | title: '어디에 살고 싶나요?',
17 | content: '',
18 | leftComment: '제주',
19 | rightComment: '육지',
20 | handleLeftButton: () => router.push('/detail/jeju'),
21 | handleRightButton: () => router.push('/detail/land'),
22 | });
23 | const { openToast, ToastModal } = useToast(`집 등록이 완료되었습니다!
24 | 니집내집과 함께 색다른 집에서 함께해요`);
25 | useEffect(() => {
26 | if (query.register) {
27 | openToast();
28 | }
29 | }, [query]);
30 | return (
31 | <>
32 |
33 |
34 |
35 |
36 |
37 |
38 | router.push('/notification')} />
39 |
40 |
41 |
42 |
43 |
44 | 집을 등록할래요.
45 |
46 |
47 |
48 |
49 |
50 | >
51 | );
52 | }
53 |
54 | export default Home;
55 |
56 | const StHeader = styled.div`
57 | display: flex;
58 | justify-content: space-between;
59 | align-items: center;
60 | width: 100%;
61 | max-width: 420px;
62 | height: 60px;
63 | margin-top: 40px;
64 | padding: 20px;
65 |
66 | .brand {
67 | width: 95px;
68 | height: 27px;
69 | }
70 |
71 | .notice {
72 | width: 27px;
73 | height: 27px;
74 | cursor: pointer;
75 | }
76 | `;
77 |
78 | const StFooter = styled.div`
79 | position: fixed;
80 | width: 100%;
81 | max-width: 420px;
82 | padding: 20px;
83 | bottom: 34px;
84 | display: flex;
85 | flex-direction: column;
86 | align-items: center;
87 |
88 | button {
89 | width: 100%;
90 | height: 61px;
91 | margin-top: 25px;
92 | border-radius: 10px;
93 | background-color: #6765ff;
94 | font-size: 17px;
95 | font-weight: 500;
96 | line-height: 27px;
97 | color: white;
98 | text-align: center;
99 | }
100 | `;
101 |
102 | const StRegisterHome = styled.a`
103 | display: flex;
104 | align-items: center;
105 | width: 160px;
106 | height: 36px;
107 | border-radius: 110px;
108 | padding: 7px 16px 7px 16px;
109 | background-color: black;
110 | cursor: pointer;
111 |
112 | .home {
113 | width: 20px;
114 | height: 20px;
115 | margin-right: 10px;
116 | }
117 |
118 | span {
119 | color: white;
120 | font-size: 14px;
121 | font-weight: 400;
122 | line-height: 22px;
123 | }
124 | `;
125 |
126 | const StHome = styled.div`
127 | width: 100%;
128 | height: 100vh;
129 | background: #282828 no-repeat center/100% url('/assets/images/img_main.png');
130 | `;
131 |
--------------------------------------------------------------------------------
/src/components/notification/Notification.tsx:
--------------------------------------------------------------------------------
1 | import { client } from '@src/services/libs/api';
2 | import { useRouter } from 'next/router';
3 | import { icCalendar, icLocationColored } from 'public/assets/icons';
4 | import styled from 'styled-components';
5 | import ImageDiv from '../common/ImageDiv';
6 |
7 | interface IProps {
8 | img: string;
9 | title: string;
10 | location: string;
11 | period: string;
12 | alarmId: number;
13 | // alarmStatus: 'ACCEPT' | 'UNREAD';
14 | status: string;
15 | userName?: string;
16 | messageLink: string;
17 | }
18 |
19 | export default function Notification(props: IProps) {
20 | const router = useRouter();
21 | const { img, title, location, alarmId, period, status, messageLink } = props;
22 | return (
23 |
24 |
25 |
26 |
27 | {title}
28 |
29 |
30 | {location}
31 |
32 |
33 |
34 | {period}
35 |
36 |
37 |
38 | {status === 'UNREAD' ? (
39 |
40 |
41 |
48 |
49 | ) : (
50 | router.push(messageLink)}>대화하기
51 | )}
52 |
53 | );
54 | }
55 |
56 | const StMainContainer = styled.div`
57 | border-bottom: 1px solid #a3a3a3;
58 | width: 100%;
59 | height: 224px;
60 | border-radius: 0px;
61 | padding: 25px 0;
62 |
63 | & > div:first-child {
64 | display: flex;
65 | }
66 |
67 | & img {
68 | object-fit: cover;
69 | border-radius: 8px;
70 | }
71 |
72 | .thumbnail {
73 | position: relative;
74 | width: 103px;
75 | height: 103px;
76 | }
77 | `;
78 | const StContentContainer = styled.div`
79 | margin-left: 22px;
80 |
81 | & > div {
82 | display: flex;
83 | align-items: center;
84 | gap: 6px;
85 | margin-bottom: 6px;
86 | font-size: 12px;
87 | font-weight: 400;
88 | color: #a3a3a3;
89 |
90 | .location,
91 | .calendar {
92 | width: 19px;
93 | height: 19px;
94 | }
95 | }
96 | `;
97 |
98 | const StTitle = styled.span`
99 | display: block;
100 | height: 44px;
101 | margin-bottom: 16px;
102 | font-size: 14px;
103 | font-weight: 500;
104 | line-height: 22px;
105 | `;
106 |
107 | const StReConversationButton = styled.button`
108 | width: 100%;
109 | height: 46px;
110 | border-radius: 10px;
111 | margin-top: 24px;
112 | background-color: black;
113 | color: white;
114 | `;
115 |
116 | const StButtonContainer = styled.div`
117 | display: flex;
118 | justify-content: space-between;
119 | gap: 25px;
120 | margin-top: 24px;
121 | button {
122 | width: 100%;
123 | height: 46px;
124 | border-radius: 10px;
125 | }
126 | button:first-child {
127 | background-color: #e9e9ff;
128 | color: #6765ff;
129 | }
130 | button:last-child {
131 | background-color: #6765ff;
132 | color: white;
133 | }
134 | `;
135 |
--------------------------------------------------------------------------------
/public/assets/images/img_farm_small.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/public/assets/icons/ic_kakao.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/images/img_farm.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/components/register/BottomSheet.tsx:
--------------------------------------------------------------------------------
1 | import useToast from '@src/hooks/useToast';
2 | import { client } from '@src/services/libs/api';
3 | import { icClose } from 'public/assets/icons';
4 | import { useEffect, useState } from 'react';
5 | import styled from 'styled-components';
6 | import ImageDiv from '../common/ImageDiv';
7 |
8 | interface BottomSheetProps {
9 | closeModal: () => void;
10 | }
11 |
12 | function BottomSheet(props: BottomSheetProps) {
13 | const { closeModal } = props;
14 | const [startDate, setStartDate] = useState('');
15 | const [lastDate, setLastDate] = useState('');
16 | const [nights, setNights] = useState(0);
17 | const [days, setDays] = useState(0);
18 | const { openToast, ToastModal } = useToast(`채팅 신청 완료!
19 | 상대방이 수락하면 이야기를 나눌 수 있습니다`);
20 |
21 | const handleSubmit = () => {
22 | client.post('/notice/send', {
23 | endDate: lastDate,
24 | receiverId: 1,
25 | senderId: 2,
26 | startDate: startDate,
27 | });
28 | openToast();
29 | };
30 | useEffect(() => {
31 | const startNumber = Number(startDate.slice(-2));
32 | const lastNumber = Number(lastDate.slice(-2));
33 | setNights(lastNumber - startNumber);
34 | setDays(lastNumber - startNumber + 1);
35 | }, [startDate, lastDate]);
36 |
37 | return (
38 | <>
39 |
40 |
41 |
42 |
45 |
46 | 언제 가고 싶어요!
47 | {startDate && lastDate && (
48 |
49 | {nights}박 {days}일
50 |
51 | )}
52 |
53 |
54 | setStartDate(e.target.value)} />
55 | -
56 | setLastDate(e.target.value)} />
57 |
58 |
59 | 1:1 대화 신청하기
60 |
61 |
62 | >
63 | );
64 | }
65 |
66 | export default BottomSheet;
67 |
68 | const StModalBackground = styled.div`
69 | position: fixed;
70 | top: 50%;
71 | left: 50%;
72 | transform: translate(-50%, -50%);
73 | width: 100%;
74 | max-width: 420px;
75 | height: 100%;
76 | z-index: 1;
77 | background-color: rgba(0, 0, 0, 0.4);
78 | `;
79 |
80 | const StBottomSheet = styled.div`
81 | z-index: 2;
82 | position: fixed;
83 | bottom: 0;
84 | width: 100%;
85 | max-width: 420px;
86 | height: 345px;
87 | padding: 0 20px;
88 | border-radius: 15px 15px 0 0;
89 | background: #fff;
90 | text-align: right;
91 |
92 | .close {
93 | width: 27px;
94 | height: 27px;
95 | padding-top: 16px;
96 | }
97 | `;
98 |
99 | const StChattingButton = styled.button<{ isSelected: boolean }>`
100 | display: block;
101 | width: 100%;
102 | height: 61px;
103 | line-height: 61px;
104 | border-radius: 80px;
105 | background-color: ${({ isSelected }) => (isSelected ? '#6765ff' : ' #E9E9FF')};
106 | color: #fff !important;
107 | margin-bottom: 48px;
108 | text-align: center;
109 | font-weight: 500;
110 | font-size: 17px;
111 | `;
112 |
113 | const StBottomSheetTitle = styled.div`
114 | display: flex;
115 | justify-content: space-between;
116 | font-weight: 700;
117 | font-size: 21px;
118 | line-height: 160.3%;
119 | margin-top: 28px;
120 | margin-bottom: 30px;
121 | `;
122 |
123 | const StDate = styled.div`
124 | height: 30px;
125 | border-radius: 130px;
126 | background-color: #e9e9ff;
127 | color: #6765ff;
128 | font-weight: 500;
129 | font-size: 14px;
130 | line-height: 160.3%;
131 | padding: 4px 12px;
132 | `;
133 |
134 | const StInputContainer = styled.div`
135 | display: flex;
136 | align-items: center;
137 | justify-content: center;
138 | margin-bottom: 25px;
139 |
140 | & > div {
141 | margin: auto 6px;
142 | }
143 | `;
144 |
--------------------------------------------------------------------------------
/public/assets/images/img_swimming.svg:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/public/assets/images/img_swimming_small.svg:
--------------------------------------------------------------------------------
1 |
36 |
--------------------------------------------------------------------------------
/public/assets/images/img_exercise_small.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/public/assets/images/img_exercise.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/pages/detail/[location].tsx:
--------------------------------------------------------------------------------
1 | import ImageDiv from '@src/components/common/ImageDiv';
2 | import SEO from '@src/components/common/SEO';
3 | import useModal from '@src/hooks/useModal';
4 | import { client } from '@src/services/libs/api';
5 | import { useRouter } from 'next/router';
6 | import { icDetailBack, icLocationColored } from 'public/assets/icons';
7 | import { useEffect, useState } from 'react';
8 | import styled from 'styled-components';
9 |
10 | export default function Detail() {
11 | const router = useRouter();
12 | const { location } = router.query;
13 | const { openModal, Modal }: any = useModal({
14 | isConfirm: false,
15 | title: '다른곳도 둘러볼까요?',
16 | content: ` 육지를 선택하면 제주의 집을,
17 | 제주를 선택하면 육지의 집을 살펴볼 수 있습니다.`,
18 | leftComment: '제주',
19 | rightComment: '육지',
20 | handleLeftButton: () => router.replace('/detail/jeju'),
21 | handleRightButton: () => router.replace('/detail/land'),
22 | });
23 | const [houseList, setHouseList] = useState([]);
24 | useEffect(() => {
25 | (async () => {
26 | const { data } = await client.get(`/house/${location === 'jeju' ? '제주' : '육지'}`);
27 | setHouseList(data);
28 | })();
29 | }, [location]);
30 |
31 | return (
32 | <>
33 |
34 |
35 |
36 |
37 | {
42 | e.stopPropagation;
43 | router.push('/');
44 | }}
45 | />
46 | {location === 'jeju' ? '제주' : '육지'}
47 | {location === 'land' && 서울, 경기, 인천, 부산, 강원 등}
48 |
49 | {houseList.map((house: any) => (
50 | router.push(`/detail/info/${house.houseId}`)}>
51 |
58 |
59 | {house.houseName}
60 |
61 |
62 | {house.address}
63 |
64 |
65 | {house.houseInfoDTO.buildingType}
66 | {house.houseInfoDTO.numberOfRooms}
67 | {house.houseInfoDTO.numberOfHouse}
68 |
69 |
70 |
71 | ))}
72 |
73 | >
74 | );
75 | }
76 |
77 | const StMainContainer = styled.div`
78 | display: flex;
79 | flex-direction: column;
80 | align-items: center;
81 | cursor: pointer;
82 |
83 | .back {
84 | width: 27px;
85 | height: 27px;
86 | margin-left: 20px;
87 | cursor: pointer;
88 | }
89 |
90 | & > div:first-child {
91 | display: flex;
92 | align-items: center;
93 | width: calc(100% - 40px);
94 | height: 59px;
95 | margin-top: 20px;
96 | margin-bottom: 26px;
97 | border-radius: 60px;
98 | box-shadow: 0px 7px 18px 0px #0000000a;
99 |
100 | h5 {
101 | line-height: 29px;
102 | margin-left: 18px;
103 | margin-right: 8px;
104 | }
105 | }
106 | `;
107 |
108 | const StAddress = styled.span`
109 | color: #a3a3a3;
110 | font-size: 14px;
111 | font-weight: 400;
112 | line-height: 22px;
113 | `;
114 |
115 | const StDetailContainer = styled.div`
116 | width: 100%;
117 |
118 | img {
119 | object-fit: cover;
120 | }
121 |
122 | .thumbnail {
123 | position: relative;
124 | width: 100%;
125 | height: 222px;
126 | }
127 | `;
128 |
129 | const StContentWrapper = styled.div`
130 | width: 100%;
131 | padding: 18px 20px 28px 20px;
132 |
133 | & > div:first-of-type {
134 | display: flex;
135 | align-items: center;
136 | margin-top: 9px;
137 | margin-bottom: 19px;
138 |
139 | .location {
140 | width: 19px;
141 | height: 19px;
142 | margin-right: 6px;
143 | }
144 |
145 | span {
146 | font-size: 12px;
147 | font-weight: 400;
148 | line-height: 19.24px;
149 | color: #a3a3a3;
150 | }
151 | }
152 |
153 | & > span {
154 | display: block;
155 | width: 100%;
156 | overflow: hidden;
157 | white-space: nowrap;
158 | text-overflow: ellipsis;
159 | font-size: 14px;
160 | font-weight: 500;
161 | line-height: 22px;
162 | }
163 | `;
164 |
165 | const StTagContainer = styled.div`
166 | & > span {
167 | height: 27px;
168 | border-radius: 130px;
169 | padding: 4px 12px 4px 12px;
170 | color: #6765ff;
171 | background-color: #eef3f9;
172 | margin-right: 8px;
173 | }
174 | `;
175 |
--------------------------------------------------------------------------------
/src/pages/register/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import styled from 'styled-components';
4 | import useModal from '@src/hooks/useModal';
5 | import Button from '@src/components/register/Button';
6 | import ImageDiv from '@src/components/common/ImageDiv';
7 | import SelectHomeImage from '@src/components/register/SelectHomeImage';
8 | import HomeInformation from '@src/components/register/HomeInformation';
9 | import HomePrecuations from '@src/components/register/HomePrecautions';
10 | import PlaceInputContainer from '@src/components/register/PlaceInputContainer';
11 | import { icBack, icCloseBg } from 'public/assets/icons';
12 | import PeopleInformation from '@src/components/register/PeopleInformation';
13 | import LinkShare from '@src/components/register/LinkShare';
14 | import SEO from '@src/components/common/SEO';
15 | import AttractionJeju from '@src/components/register/Attraction/AttractionJeju';
16 |
17 | export default function Register() {
18 | const router = useRouter();
19 | const [pageIdx, setPageIdx] = useState(0);
20 | const [files, setFiles] = useState([]);
21 | const [images, setImages] = useState([]);
22 | const [representImg, setRepresentImg] = useState(null);
23 | const [nextValid, setNextValid] = useState(false);
24 |
25 | const handleClickPrevious = () => {
26 | //todo : 페이지 인덱스에 따라 다르게 동작
27 | if (pageIdx === 1 && files.length !== 0) {
28 | setFiles([]);
29 | setImages([]);
30 | setNextValid(false);
31 | return;
32 | }
33 | if (pageIdx === 0) {
34 | router.push('/');
35 | return;
36 | }
37 | setNextValid(false);
38 | setPageIdx((prev) => prev - 1);
39 | };
40 |
41 | const handleClickNext = () => {
42 | setPageIdx((prev) => prev + 1);
43 | setNextValid(false);
44 | };
45 |
46 | const handleClickSubmit = () => {
47 | //todo : 오브젝트 형태로 넘겨주기
48 | router.push('/?register=true');
49 | };
50 |
51 | const isNotEssential = (pageNum: number) => {
52 | // 필수 항목 아닌 경우
53 | const notEssentialList = [3, 5];
54 | return notEssentialList.includes(pageNum);
55 | };
56 | const pages = [
57 | ,
58 | ,
66 | ,
67 | ,
68 | ,
69 | ,
70 | ,
71 | ];
72 |
73 | const { openModal, Modal }: any = useModal({
74 | isConfirm: true,
75 | title: '집 등록을 그만둡니다.',
76 | content: ` 나가기를 하면 현재 페이지까지
77 | 입력한 정보는 저장되지 않습니다.`,
78 | leftComment: '나가기',
79 | rightComment: '취소',
80 | handleLeftButton: () => router.push('/'),
81 | });
82 |
83 | return (
84 | <>
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {pageIdx + 1} / {pages.length}
93 |
94 |
95 |
96 |
97 |
{pages[pageIdx]}
98 |
99 | {isNotEssential(pageIdx) && 필수 항목이 아닙니다.}
100 | {pageIdx + 1 !== pages.length ? (
101 |
102 | ) : (
103 |
104 | )}
105 |
106 |
107 | >
108 | );
109 | }
110 |
111 | const StFooter = styled.div`
112 | padding: 0 20px;
113 | width: 100%;
114 | max-width: 420px;
115 | height: 107px;
116 | display: flex;
117 | flex-direction: column;
118 | text-align: center;
119 | position: fixed;
120 | margin: 0 auto;
121 | bottom: 0px;
122 | padding-bottom: 46px;
123 | background-color: white;
124 |
125 | & > span {
126 | font-size: 12px;
127 | line-height: 160.3%;
128 | color: #6765ff;
129 | }
130 | `;
131 |
132 | const StMainContent = styled.div`
133 | overflow: auto;
134 | margin-bottom: 108px;
135 | `;
136 |
137 | const StHeader = styled.div`
138 | display: flex;
139 | align-items: center;
140 | justify-content: space-between;
141 | padding: 0 20px;
142 | height: 60px;
143 |
144 | & > div {
145 | display: flex;
146 | }
147 |
148 | .back,
149 | .exit {
150 | width: 27px;
151 | height: 27px;
152 | cursor: pointer;
153 | }
154 | `;
155 |
156 | const StPageCounter = styled.div`
157 | width: 55px;
158 | height: 27px;
159 | line-height: 27px;
160 | border-radius: 29px;
161 | margin-right: 11px;
162 | background-color: #eeeeee;
163 | text-align: center;
164 | font-weight: 500;
165 |
166 | & > span {
167 | color: #6765ff;
168 | }
169 | `;
170 |
--------------------------------------------------------------------------------
/src/pages/detail/info/[id].tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Slider from 'react-slick';
3 | import { imgExercise, imgFarm, imgOcean, imgRoad, imgSwimming, imgUpload } from 'public/assets/images';
4 | import ImageDiv from '@src/components/common/ImageDiv';
5 | import 'slick-carousel/slick/slick.css';
6 | import 'slick-carousel/slick/slick-theme.css';
7 | import { useEffect, useState } from 'react';
8 | import { icBack, icLike, icMark } from 'public/assets/icons';
9 | import BottomSheet from '@src/components/register/BottomSheet';
10 | import SEO from '@src/components/common/SEO';
11 | import { useRouter } from 'next/router';
12 | import { client } from '@src/services/libs/api';
13 |
14 | export default function InfoDetail() {
15 | const [imgIdx, setImgIdx] = useState(0);
16 | const router = useRouter();
17 | const [detailInfo, setDetailInfo] = useState();
18 | useEffect(() => {
19 | (async () => {
20 | const { data } = await client.get(`/house/detail/${router.query.id}`);
21 | setDetailInfo(data);
22 | })();
23 | }, []);
24 | const settings = {
25 | dots: false,
26 | infinite: true,
27 | speed: 500,
28 | slidesToShow: 1,
29 | slidesToScroll: 1,
30 | arrows: false,
31 | beforeChange: (oldIdx: any, newIdx: number) => {
32 | setImgIdx(newIdx);
33 | },
34 | };
35 | const [isModalOpen, setIsModalOpen] = useState(false);
36 |
37 | return (
38 | <>
39 |
40 |
41 |
42 | router.back()} src={icBack} className="back" alt="뒤로 가기" />
43 |
44 | {detailInfo?.imagePaths.map((image: any, idx: any) => (
45 |
46 |
53 |
54 | ))}
55 |
56 |
57 | {imgIdx + 1}/{detailInfo?.imagePaths.length}
58 |
59 |
60 |
61 | {detailInfo?.houseName}
62 |
63 |
64 | {detailInfo?.address}
65 |
66 |
67 | {detailInfo?.houseInfoDTO.buildingType}
68 | {detailInfo?.houseInfoDTO.availablePeople}인
69 | {detailInfo?.houseInfoDTO.numberOfHouse}
70 |
71 | 집을 소개합니다
72 | {detailInfo?.houseIntroduction}
73 | 주의해주세요!
74 | {detailInfo?.precautionList.join(' ')}
75 | 근처에서 이렇게 놀아요
76 |
77 |
78 |
79 | 해변
80 |
81 |
82 |
83 | 올레길
84 |
85 |
86 |
87 | 감귤농장
88 |
89 |
90 | 빌려 드립니다
91 |
92 |
93 |
94 | 운동기구
95 |
96 |
97 |
98 | 물놀이 용품
99 |
100 |
101 |
105 |
106 | {isModalOpen && setIsModalOpen(false)} />}
107 |
108 | >
109 | );
110 | }
111 |
112 | const StMainContainer = styled.div`
113 | width: 100%;
114 | `;
115 | const StSliderWrapper = styled.div`
116 | & > span {
117 | position: relative;
118 | float: right;
119 | right: 20px;
120 | display: block;
121 | margin-top: -50px;
122 | width: 55px;
123 | height: 27px;
124 | border-radius: 29px;
125 | background-color: black;
126 | color: white;
127 | text-align: center;
128 | line-height: 27px;
129 | }
130 | .back {
131 | position: absolute;
132 | z-index: 999;
133 | margin: 20px 0 0 30px;
134 | cursor: pointer;
135 | }
136 | `;
137 | const StImageWrapper = styled.div`
138 | display: flex;
139 | width: 100%;
140 | height: 314px;
141 | text-align: center;
142 | align-items: center;
143 |
144 | img {
145 | object-fit: cover;
146 | }
147 |
148 | .thumbnail {
149 | position: relative;
150 | width: 100%;
151 | max-height: 314px;
152 | height: 314px;
153 | }
154 | `;
155 |
156 | const StDetailWrapper = styled.div`
157 | padding: 0 20px;
158 | font-weight: 400;
159 | font-size: 14px;
160 | line-height: 22px;
161 |
162 | button {
163 | display: flex;
164 | width: 100%;
165 | height: 61px;
166 | background: #6765ff;
167 | color: #fff;
168 | border-radius: 80px;
169 | align-items: center;
170 | justify-content: center;
171 | margin-top: 93px;
172 | margin-bottom: 46px;
173 |
174 | .like {
175 | width: 27px;
176 | height: 27px;
177 | }
178 | }
179 | `;
180 |
181 | const StDetailTitle = styled.div`
182 | font-weight: 700;
183 | font-size: 18px;
184 | line-height: 177.8%;
185 | color: #101223;
186 | margin-top: 30px;
187 | margin-bottom: 8px;
188 | `;
189 |
190 | const StPlace = styled.div`
191 | display: flex;
192 | gap: 6px;
193 | font-size: 12px;
194 | line-height: 160.3%;
195 | color: #a3a3a3;
196 | margin-bottom: 25px;
197 | `;
198 |
199 | const StTagList = styled.div`
200 | display: flex;
201 | gap: 8px;
202 | margin-bottom: 60px;
203 |
204 | span {
205 | padding: 4px 12px;
206 | color: #6765ff;
207 | background: #e9e9ff;
208 | border-radius: 130px;
209 | }
210 | `;
211 |
212 | const StSubtitle = styled.div`
213 | font-weight: 500;
214 | font-size: 18px;
215 | line-height: 160.3%;
216 | color: #101223;
217 | margin-bottom: 20px;
218 | `;
219 |
220 | const StAttraction = styled.div`
221 | display: flex;
222 | gap: 16px;
223 | justify-content: flex-start;
224 | text-align: center;
225 | margin-bottom: 60px;
226 | font-weight: 500;
227 | font-size: 16px;
228 | line-height: 160.3%;
229 |
230 | span {
231 | display: block;
232 | margin-top: 9px;
233 | }
234 |
235 | .attraction {
236 | width: 96px;
237 | height: 96px;
238 | }
239 | `;
240 |
241 | const StContent = styled.div`
242 | font-weight: 400;
243 | font-size: 14px;
244 | line-height: 22px;
245 | margin-bottom: 60px;
246 | white-space: pre-wrap;
247 | `;
248 |
249 | const StWarning = styled.div`
250 | background: #fef2f2;
251 | border-radius: 10px;
252 | padding: 20px;
253 | color: #ef4040;
254 | margin-bottom: 80px;
255 | `;
256 |
--------------------------------------------------------------------------------
/public/assets/images/img_culture_small.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/public/assets/images/img_activity_small.svg:
--------------------------------------------------------------------------------
1 |
40 |
--------------------------------------------------------------------------------
/public/assets/images/img_hot_place_small.svg:
--------------------------------------------------------------------------------
1 |
81 |
--------------------------------------------------------------------------------