diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..ecd06c2 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,59 @@ +name: CD + +on: + push: + branches: + - main + +jobs: + build: + name: build & deploy + runs-on: ubuntu-latest + steps: + - name: checkout Github Action + uses: actions/checkout@v3 + + - name: Get npm cache directory + id: npm-cache-dir + run: | + echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v3 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: install yarn dependencies + run: yarn install + + - name: react build + run: yarn run build + + # aws에 접근하기 위한 권한을 받아옵니다. + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_S3_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_S3_SECRET_KEY }} + aws-region: ap-northeast-2 + + # S3에 build 파일을 올립니다. + - name: Upload to S3 + env: + BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME}} + run: | + aws s3 sync \ + ./build s3://$BUCKET_NAME + + # cloudfront로 배포되는 파일은 기본설정 상 24시간동안 캐시가 유지됩니다. + # 배포 후 S3에는 최신 정적리소스가 올라가있지만 엣지로케이션엔 이전 파일이 올라가있는 상태라는 의미입니다. + # 바로 변화가 반영되길 바란다면 invalidation을 해주면 됩니다. + # 해당 부분은 과금될 수 있으니 확인 후 사용하세요! + - name: CloudFront Invalidation + env: + CLOUD_FRONT_ID: ${{ secrets.AWS_CLOUDFRONT_ID}} + run: | + aws cloudfront create-invalidation \ + --distribution-id $CLOUD_FRONT_ID --paths /* diff --git a/src/apis/topic/useTopics.ts b/src/apis/topic/useTopics.ts index 6de0df0..461c9e3 100644 --- a/src/apis/topic/useTopics.ts +++ b/src/apis/topic/useTopics.ts @@ -1,11 +1,6 @@ import { useMutation, useQuery } from '@tanstack/react-query'; -import { - CHOICE_OPTIONS, - ChoiceContent, - TopicCreateRequestDTO, - TopicResponse, -} from '@interfaces/api/topic'; +import { TopicCreateRequestDTO, TopicResponse } from '@interfaces/api/topic'; import { PagingDataResponse } from '@interfaces/api'; @@ -14,7 +9,7 @@ import client from '@apis/fetch'; export const TOPIC_KEY = 'topics'; const getTopics = () => { - return client.get>('/topics/info/voting?size=100'); + return client.get>('/topics/info?size=100'); }; const useTopics = () => { diff --git a/src/interfaces/api/topic.ts b/src/interfaces/api/topic.ts index 78e517d..15fcd7b 100644 --- a/src/interfaces/api/topic.ts +++ b/src/interfaces/api/topic.ts @@ -18,10 +18,10 @@ interface TopicResponse { export interface TopicCreateRequestDTO { side: string; - keywordName: string; + keywordName: string | null; title: string; choices: ChoiceRequest[]; - deadline: number; + deadline: number | null; } interface ChoiceRequest { diff --git a/src/routes/Topic/Create/ASide/ATopicCreate.tsx b/src/routes/Topic/Create/ASide/ATopicCreate.tsx index 4ac5011..520904e 100644 --- a/src/routes/Topic/Create/ASide/ATopicCreate.tsx +++ b/src/routes/Topic/Create/ASide/ATopicCreate.tsx @@ -1,12 +1,16 @@ import React, { useState, useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import { useCreateTopics } from '@apis/topic/useTopics'; import DefaultButton from '@components/commons/Button/DefaultButton'; import { Col } from '@components/commons/Flex/Flex'; import Text from '@components/commons/Text/Text'; import TextInput from '@components/commons/TextInput/TextInput'; import { theme3 } from '@components/commons/TextInput/theme'; import TopicCreateTextInput from '@components/TopicCreate/TopicCreateTextInput'; +import { CHOICE_OPTIONS } from '@interfaces/api/topic'; +import { TopicCreateDTO } from '@routes/Topic/Create/TopicCreate'; import { INPUT_TYPE, CONFIG } from '@constants/form'; @@ -14,34 +18,64 @@ import { colors } from '@styles/theme'; import { Container, SubmitButton } from './ATopicCreate.styles'; -interface TopicCreateDTO { - topicTitle: string; - ATopic: string; - BTopic: string; -} - const ATopicCreate = () => { + const navigate = useNavigate(); const methods = useForm({ mode: 'onChange' }); const titleProgress = methods.watch(INPUT_TYPE.TOPIC_TITLE) ? `${methods.watch(INPUT_TYPE.TOPIC_TITLE)?.length}/20` : '0/20'; const [isFormFilled, setIsFormFilled] = useState(false); - const handleSubmitButtonClick = () => { - console.log('submit'); + + const createTopicMutation = useCreateTopics(); + + const handleSubmitForm = async () => { + const data = methods.getValues(); + try { + const res = await createTopicMutation.mutateAsync({ + side: 'TOPIC_A', + title: data.topicTitle, + keywordName: null, + deadline: null, + choices: [ + { + choiceContentRequest: { + text: data.ATopic, + type: 'IMAGE_TEXT', + imageUrl: null, + }, + choiceOption: CHOICE_OPTIONS.CHOICE_A, + }, + { + choiceContentRequest: { + text: data.BTopic, + type: 'IMAGE_TEXT', + imageUrl: null, + }, + choiceOption: CHOICE_OPTIONS.CHOICE_B, + }, + ], + }); + navigate(`/topics/a`); + } catch (error) { + console.error(error); + } }; useEffect(() => { + const ATopicCondition = methods.getFieldState(INPUT_TYPE.A_TOPIC, methods.formState); + const BTopicCondition = methods.getFieldState(INPUT_TYPE.B_TOPIC, methods.formState); if ( - methods.getValues(INPUT_TYPE.TOPIC_TITLE) && - methods.getValues(INPUT_TYPE.A_TOPIC) && - methods.getValues(INPUT_TYPE.B_TOPIC) + !ATopicCondition.invalid && + !BTopicCondition.invalid && + ATopicCondition.isDirty && + BTopicCondition.isDirty ) { setIsFormFilled(true); } else { setIsFormFilled(false); } - }, [methods]); + }, [methods.formState, methods]); return ( @@ -69,7 +103,7 @@ const ATopicCreate = () => { diff --git a/src/routes/Topic/Create/BSide/BTopicCreate.tsx b/src/routes/Topic/Create/BSide/BTopicCreate.tsx index e528713..4c28e96 100644 --- a/src/routes/Topic/Create/BSide/BTopicCreate.tsx +++ b/src/routes/Topic/Create/BSide/BTopicCreate.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from 'react'; import { FormProvider, SubmitHandler, useForm, useWatch } from 'react-hook-form'; -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { useCreateTopics } from '@apis/topic/useTopics'; import DefaultButton from '@components/commons/Button/DefaultButton'; import { CHOICE_OPTIONS, TopicCreateRequestDTO } from '@interfaces/api/topic'; +import { TopicCreateDTO } from '@routes/Topic/Create/TopicCreate'; import { INPUT_TYPE } from '@constants/form'; @@ -18,18 +19,8 @@ import { import BTopicCreateStep1 from './BTopicCreateStep1'; import BTopicCreateStep2 from './BTopicCreateStep2'; -interface TopicCreateDTO { - topicTitle: string; - ATopic: string; - BTopic: string; - topicCategory: string; - ATopicImageURL: string; - BTopicImageURL: string; - topicDeadline: number; - topicType: string; -} - const BTopicCreate = () => { + const navigate = useNavigate(); const methods = useForm({ mode: 'onChange' }); const contentType = useWatch({ control: methods.control, @@ -77,7 +68,7 @@ const BTopicCreate = () => { }, ], }); - console.log('success :', res); + navigate(`/topics/b`); } catch (error) { console.error(error); } diff --git a/src/routes/Topic/Create/TopicCreate.tsx b/src/routes/Topic/Create/TopicCreate.tsx index 2fcb355..994ada8 100644 --- a/src/routes/Topic/Create/TopicCreate.tsx +++ b/src/routes/Topic/Create/TopicCreate.tsx @@ -18,6 +18,17 @@ import { SideChangeButton, } from './TopicCreate.sytles'; +export interface TopicCreateDTO { + topicTitle: string; + ATopic: string; + BTopic: string; + topicCategory: string; + ATopicImageURL: string; + BTopicImageURL: string; + topicDeadline: number; + topicType: string; +} + const TopicCreate = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams();