diff --git a/.github/workflows/backend_dev_cd.yml b/.github/workflows/backend_dev_cd.yml new file mode 100644 index 00000000..ade0b4b1 --- /dev/null +++ b/.github/workflows/backend_dev_cd.yml @@ -0,0 +1,180 @@ +name: Backend Dev CD + +on: + workflow_dispatch: + push: + branches: + - dev + paths: + - backend/** + +jobs: + build: + name: 🏗️ Build Jar and Upload Docker Image + environment: dev + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.SUBMODULE_GITHUB_TOKEN }} + + - name: 🏗️ Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: 21 + + - name: 🏗️ Set up Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: 🏗️ Build with Gradle + run: ./gradlew clean bootJar + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker Image Build and Push + uses: docker/build-push-action@v6 + with: + context: ./backend + push: true + tags: ${{ secrets.DOCKER_REPOSITORY_NAME }}:${{ github.sha }} + platforms: linux/arm64 + + deploy: + name: 🚀 Server Deployment + environment: dev + needs: build + runs-on: [ self-hosted, develup-dev ] + defaults: + run: + working-directory: backend + + env: + BACKEND_APP_IMAGE_NAME: ${{ secrets.DOCKER_REPOSITORY_NAME }}:${{ github.sha }} + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} + MYSQL_ROOT_HOST: ${{ secrets.MYSQL_ROOT_HOST }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.SUBMODULE_GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker Compose up + run: docker compose -f compose.dev.yml up -d + + - name: Clean Unused Image + run: docker image prune -af + + slack-notify_success: + runs-on: ubuntu-latest + environment: dev + needs: + - build + - deploy + if: success() + steps: + - name: Extract Commit Title + run: | + COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + + - name: Build and Deploy Success + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "Build and Deploy Status", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀 Build Success \n\t • 🟢 Deploy Success \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + + slack-notify_build-fail: + runs-on: ubuntu-latest + environment: dev + needs: + - build + if: failure() + steps: + - name: Extract Commit Title + run: | + COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + + - name: Build Fail + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "Build and Deploy Status", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🔴 Build Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + + slack-notify_deploy-fail: + runs-on: ubuntu-latest + environment: dev + needs: + - deploy + if: failure() + steps: + - name: Extract Commit Title + run: | + COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + + - name: Deploy Fail + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "Build and Deploy Status", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀Build Success \n\t • 🔴Deploy Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/.github/workflows/backend_dev_ci.yml b/.github/workflows/backend_dev_ci.yml new file mode 100644 index 00000000..1c1f119a --- /dev/null +++ b/.github/workflows/backend_dev_ci.yml @@ -0,0 +1,95 @@ +name: 🚛 Backend Dev CI + +on: + pull_request: + types: [ opened, reopened, synchronize ] + branches: + - dev + +jobs: + PATH_CHANGES: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + backend: ${{ steps.changes.outputs.backend }} + + steps: + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + backend: + - 'backend/**' + + BE_CI: + environment: dev + runs-on: ubuntu-latest + needs: PATH_CHANGES + if: ${{ needs.PATH_CHANGES.outputs.backend == 'true' }} + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Test with Gradle + id: gradle_test + working-directory: backend + run: ./gradlew test + + BE_SLACK_MESSAGE: + environment: dev + runs-on: ubuntu-latest + needs: BE_CI + if: ${{!cancelled() && needs.BE_CI.result != 'skipped'}} + + env: + lilychoibb: ${{secrets.LILYCHOIBB_SLACK_ID}} + robinjoon: ${{secrets.ROBINJOON_SLACK_ID}} + brgndyy: ${{secrets.BRGNDYY_SLACK_ID}} + chosim-dvlpr: ${{secrets.CHOSIM_DVLPR_SLACK_ID}} + Minjoo522: ${{secrets.MINJOO522_SLACK_ID}} + alstn113: ${{secrets.ALSTN113_SLACK_ID}} + le2sky: ${{secrets.LE2SKY_SLACK_ID}} + Parkhanyoung: ${{secrets.PARKHANYOUNG_SLACK_ID}} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Get teamMember + id: teamMember + run: | + echo "SENDER_SLACK_ID=${{ env[github.event.sender.login] }}" >> $GITHUB_ENV + + - name: Slack mention + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "pr 테스트 결과", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "pr 테스트 ${{ needs.BE_CI.result }} \n • 링크: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> \n • pr 담당자: <@${{ env.SENDER_SLACK_ID }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/.github/workflows/frontend_dev_ci.yml b/.github/workflows/frontend_dev_ci.yml new file mode 100644 index 00000000..19acf1b0 --- /dev/null +++ b/.github/workflows/frontend_dev_ci.yml @@ -0,0 +1,138 @@ +name: 🎨 Frontend Dev CI + +on: + pull_request: + types: [opened, reopened, synchronize] + branches: + - dev + +env: + lilychoibb: ${{secrets.LILYCHOIBB_SLACK_ID}} + robinjoon: ${{secrets.ROBINJOON_SLACK_ID}} + brgndyy: ${{secrets.BRGNDYY_SLACK_ID}} + chosim-dvlpr: ${{secrets.CHOSIM_DVLPR_SLACK_ID}} + Minjoo522: ${{secrets.MINJOO522_SLACK_ID}} + alstn113: ${{secrets.ALSTN113_SLACK_ID}} + le2sky: ${{secrets.LE2SKY_SLACK_ID}} + Parkhanyoung: ${{secrets.PARKHANYOUNG_SLACK_ID}} + +jobs: + PATH_CHANGES: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + frontend: ${{ steps.changes.outputs.frontend }} + + steps: + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + frontend: + - 'frontend/**' + + FE_CI: + runs-on: ubuntu-latest + needs: PATH_CHANGES + if: ${{ needs.PATH_CHANGES.outputs.frontend == 'true' }} + permissions: + contents: read + packages: write + actions: write + + outputs: + lint: ${{ steps.npm_run_lint_result.outputs.result }} + build: ${{ steps.npm_run_build_result.outputs.result }} + test: ${{ steps.npm_run_test_result.outputs.result }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.15.1' + + - name: Install dependencies + run: | + cd frontend + npm install + + - name: Run lint + continue-on-error: true + id: npm_run_lint + run: | + cd frontend + npm run lint + + - name: Save Run lint result + continue-on-error: true + id: npm_run_lint_result + run: | + echo "result=${{steps.npm_run_lint.outcome}}" >> $GITHUB_OUTPUT + + - name: Run build + continue-on-error: true + id: npm_run_build + run: | + pwd + cd frontend + npm run build + + - name: Save Run build result + continue-on-error: true + id: npm_run_build_result + run: | + echo "result=${{steps.npm_run_build.outcome}}" >> $GITHUB_OUTPUT + + - name: Run test + continue-on-error: true + id: npm_run_test + run: | + cd frontend + npm run test + + - name: Save Run test result + continue-on-error: true + id: npm_run_test_result + run: | + echo "result=${{steps.npm_run_test.outcome}}" >> $GITHUB_OUTPUT + + FE_SLACK_MESSAGE: + runs-on: ubuntu-latest + needs: FE_CI + if: ${{needs.FE_CI.result != 'skipped'}} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Get teamMember + id: teamMember + run: | + echo "SENDER_SLACK_ID=${{ env[github.event.sender.login] }}" >> $GITHUB_ENV + + - name: Slack mention + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "pr 테스트 결과", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "pr 테스트 결과\n lint : ${{ needs.FE_CI.outputs.lint }} \n build : ${{ needs.FE_CI.outputs.build }} \n test : ${{ needs.FE_CI.outputs.test }} \n • 링크: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> \n • pr 담당자: <@${{ env.SENDER_SLACK_ID }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/backend/build.gradle b/backend/build.gradle index 0672379d..795c743f 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' // jwt implementation "io.jsonwebtoken:jjwt-api:${JJWT_VERSION}" diff --git a/backend/compose.dev.yml b/backend/compose.dev.yml new file mode 100644 index 00000000..6c2b4483 --- /dev/null +++ b/backend/compose.dev.yml @@ -0,0 +1,45 @@ +services: + nginx: + image: nginx + depends_on: + - application + networks: + - nginx-app-net + ports: + - "80:80" + - "443:443" + volumes: + - /home/ubuntu/custom.conf:/etc/nginx/conf.d/default.conf + - /etc/letsencrypt/live/dev.api.devel-up.co.kr/fullchain.pem:/etc/letsencrypt/live/dev.api.devel-up.co.kr/fullchain.pem + - /etc/letsencrypt/live/dev.api.devel-up.co.kr/privkey.pem:/etc/letsencrypt/live/dev.api.devel-up.co.kr/privkey.pem + + application: + image: ${BACKEND_APP_IMAGE_NAME} + depends_on: + - mysql + networks: + - nginx-app-net + - app-mysql-net + ports: + - "8080:8080" + environment: + TZ: "Asia/Seoul" + SPRING_PROFILE: dev + restart: always + container_name: develup-app + + mysql: + image: mysql + networks: + - app-mysql-net + ports: + - "3306:3306" + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_ROOT_HOST: ${MYSQL_ROOT_HOST} + container_name: develup-db-dev + +networks: + nginx-app-net: + app-mysql-net: diff --git a/backend/compose.yml b/backend/compose.yml index 35b50b89..8ca024d1 100644 --- a/backend/compose.yml +++ b/backend/compose.yml @@ -22,7 +22,7 @@ services: - "8082:8082" environment: TZ: "Asia/Seoul" - SPRING_PROFILE: dev + SPRING_PROFILE: prod restart: always container_name: develup-app diff --git a/backend/secrets b/backend/secrets index 8ad132f7..f4a6bb66 160000 --- a/backend/secrets +++ b/backend/secrets @@ -1 +1 @@ -Subproject commit 8ad132f73273f382bf81c26505565780266ed303 +Subproject commit f4a6bb669d397a13757622aa1970db8712a033e7 diff --git a/backend/src/main/java/develup/api/config/WebConfig.java b/backend/src/main/java/develup/api/config/WebConfig.java index feb8ed04..5cea1da2 100644 --- a/backend/src/main/java/develup/api/config/WebConfig.java +++ b/backend/src/main/java/develup/api/config/WebConfig.java @@ -2,6 +2,7 @@ import java.util.List; import develup.api.auth.AuthArgumentResolver; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; @@ -10,6 +11,11 @@ @Configuration public class WebConfig implements WebMvcConfigurer { + @Value("${client-host}") + private String clientHost; + @Value("${api-host}") + private String apiHost; + private final AuthArgumentResolver authArgumentResolver; public WebConfig(AuthArgumentResolver authArgumentResolver) { @@ -19,9 +25,8 @@ public WebConfig(AuthArgumentResolver authArgumentResolver) { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "http://3.38.11.109", "https://www.devel-up.co.kr", - "https://devel-up.co.kr") - .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH") + .allowedOrigins(clientHost, apiHost) + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") .allowCredentials(true); } diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index be8486af..8f92ee6c 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -7,33 +7,44 @@ VALUES ('test1@gmail.com', 'GITHUB', '1234', '구름', 'https://avatars.githubus '2024-08-16 13:40:00'); INSERT INTO mission (title, thumbnail, summary, url) -VALUES ('루터회관 흡연 단속', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-smoking.png', - '담배피다 걸린 행성이를 위한 벌금 계산 미션', 'https://github.com/develup-mission/java-smoking'), +VALUES ('주문', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-order.png', + '배달 주문을 받아보자', 'https://github.com/develup-mission/java-order'), ('숫자 맞추기 게임', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-guessing-number.png', - '숫자를 맞춰보자', 'https://github.com/develup-mission/java-guessing-number'); + '숫자를 맞춰보자', 'https://github.com/develup-mission/java-guessing-number'), + ('미로 탈출', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-maze.png', + '미노타우로스를 피해 미로에서 탈출하세요!', 'https://github.com/develup-mission/java-maze'), + ('엘리베이터 시뮬레이션', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-elevator.png', + '엘리베이터를 만들어봐요.', 'https://github.com/develup-mission/java-elevator'), + ('단어 퍼즐 게임', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/java-word-puzzle.png', + '단어의 퍼즐들을 맞춰주세요!', 'https://github.com/develup-mission/java-word-puzzle'), + ('리액트 회원가입/로그인 폼', 'https://raw.githubusercontent.com/develup-mission/docs/main/image/react-auth-form.png', + '리액트 회원가입/로그인 폼을 구현해봐요.', 'https://github.com/develup-mission/react-auth-form'); INSERT INTO hash_tag (name) VALUES ('JAVA'), ('객체지향'), - ('TDD'), ('클린코드'), - ('레벨1'), - ('잠실캠퍼스'), - ('선릉캠퍼스'); + ('TYPESCRIPT'), + ('REACT'); INSERT INTO mission_hash_tag (mission_id, hash_tag_id) VALUES (1, 1), (1, 2), (1, 3), - (1, 4), - (1, 5), - (1, 6), (2, 1), (2, 2), (2, 3), - (2, 4), - (2, 5), - (2, 7); + (3, 1), + (3, 2), + (3, 3), + (4, 1), + (4, 2), + (4, 3), + (5, 1), + (5, 2), + (5, 3), + (6, 4), + (6, 5); INSERT INTO solution (mission_id, member_id, title, description, url, status, created_at) VALUES (1, 1, '릴리 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', diff --git a/backend/src/test/java/develup/api/SolutionCommentApiTest.java b/backend/src/test/java/develup/api/SolutionCommentApiTest.java index 9e613c51..95724f6a 100644 --- a/backend/src/test/java/develup/api/SolutionCommentApiTest.java +++ b/backend/src/test/java/develup/api/SolutionCommentApiTest.java @@ -65,7 +65,7 @@ void getMyComments() throws Exception { .andExpect(jsonPath("$.data[0].id", equalTo(1))) .andExpect(jsonPath("$.data[0].solutionId", equalTo(1))) .andExpect(jsonPath("$.data[0].content", equalTo("댓글 내용"))) - .andExpect(jsonPath("$.data[0].createdAt", equalTo(now.toString()))) + .andExpect(jsonPath("$.data[0].createdAt").exists()) .andExpect(jsonPath("$.data[0].solutionTitle", equalTo("솔루션 제목"))) .andExpect(jsonPath("$.data[0].solutionCommentCount", equalTo(123))); } diff --git a/frontend/index.html b/frontend/index.html index e858b214..ea1fa9d3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,29 +1,26 @@ - - - - - - - - + - 🚀 devel-up - - -
- - + gtag('config', 'G-WJQBS2WK9R'); + + 🚀 devel-up + + +
+ + diff --git a/frontend/src/apis/clients/develupClient.ts b/frontend/src/apis/clients/develupClient.ts index 4de90e11..be1e17e3 100644 --- a/frontend/src/apis/clients/develupClient.ts +++ b/frontend/src/apis/clients/develupClient.ts @@ -1,7 +1,7 @@ import { BASE_URL } from '../baseUrl'; import APIClient from './APIClient'; -const API_URL = process.env.NODE_ENV === 'development' ? BASE_URL.dev : BASE_URL.prod; +export const API_URL = process.env.NODE_ENV === 'development' ? BASE_URL.dev : BASE_URL.prod; const header = { 'Content-type': 'application/json', diff --git a/frontend/src/apis/commentAPI.ts b/frontend/src/apis/commentAPI.ts index af9dab3e..97ef49a7 100644 --- a/frontend/src/apis/commentAPI.ts +++ b/frontend/src/apis/commentAPI.ts @@ -1,6 +1,6 @@ -import type { Comment } from '@/types'; +import type { Comment, MyComments } from '@/types'; import { develupAPIClient } from './clients/develupClient'; -import { PATH_FORMATTER } from './paths'; +import { PATH, PATH_FORMATTER } from './paths'; export const getComments = async (solutionId: number): Promise => { const { data } = await develupAPIClient.get<{ data: Comment[] }>( @@ -45,3 +45,9 @@ export const postComment = async ({ return data; }; + +export const getMyComments = async (): Promise => { + const { data } = await develupAPIClient.get<{ data: MyComments[] }>(PATH.myComments); + + return data; +}; diff --git a/frontend/src/apis/missionAPI.ts b/frontend/src/apis/missionAPI.ts index 7875317b..69d53723 100644 --- a/frontend/src/apis/missionAPI.ts +++ b/frontend/src/apis/missionAPI.ts @@ -7,9 +7,9 @@ import type { Submission, HashTag, } from '@/types'; -import MissionListInProgress from '@/mocks/missionInProgress.json'; import { populateMissionDescription } from './utils/populateMissionDescription'; import { HASHTAGS } from '@/constants/hashTags'; +import type { MissionInProgress } from '@/types/mission'; interface getMissionByIdResponse { data: MissionWithDescription; @@ -38,18 +38,14 @@ export const getMissionById = async (id: number): Promise { - // const { data } = await develupAPIClient.get(PATH.missionInProgress); - - // console.log('data : ', data); + const { data } = await develupAPIClient.get(PATH.missionInProgress); - return MissionListInProgress; - // return []; + return data; }; export interface PostSubmissionResponse { diff --git a/frontend/src/apis/paths.ts b/frontend/src/apis/paths.ts index 78ea2ec5..477854ab 100644 --- a/frontend/src/apis/paths.ts +++ b/frontend/src/apis/paths.ts @@ -9,9 +9,12 @@ export const PATH = { solutionSummaries: '/solutions', submitSolution: '/solutions/submit', startSolution: '/solutions/start', + myComments: '/solutions/comments/mine', logout: '/auth/logout', hashTags: '/hash-tags', missionInProgress: '/missions/in-progress', + solutions: '/solutions', + mySolutions: '/solutions/mine', }; export const PATH_FORMATTER = { diff --git a/frontend/src/apis/solutions.ts b/frontend/src/apis/solutions.ts index 63d3182a..8e9bd268 100644 --- a/frontend/src/apis/solutions.ts +++ b/frontend/src/apis/solutions.ts @@ -1,7 +1,6 @@ import { develupAPIClient } from '@/apis/clients/develupClient'; import { PATH } from '@/apis/paths'; import { HASHTAGS } from '@/constants/hashTags'; -import SubmittedSolutions from '@/mocks/SubmittedSolutions.json'; import type { HashTag } from '@/types'; import type { Solution, SubmittedSolution } from '@/types/solution'; @@ -29,6 +28,18 @@ export const getSolutionSummaries = async ( return data; }; +interface GetSolutionResponse { + data: Solution; +} + +export const getSolutionById = async (solutionId: number): Promise => { + const { data } = await develupAPIClient.get( + `${PATH.solutions}/${solutionId}`, + ); + + return data; +}; + export interface PostSolutionResponse { data: Solution; } @@ -55,5 +66,7 @@ export interface GetSubmittedSolution { } export const getSubmittedSolution = async (): Promise => { - return SubmittedSolutions; + const { data } = await develupAPIClient.get(PATH.mySolutions); + + return data; }; diff --git a/frontend/src/assets/images/1.png b/frontend/src/assets/images/1.png new file mode 100644 index 00000000..298f1da3 Binary files /dev/null and b/frontend/src/assets/images/1.png differ diff --git a/frontend/src/assets/images/1.svg b/frontend/src/assets/images/1.svg new file mode 100644 index 00000000..68d61d92 --- /dev/null +++ b/frontend/src/assets/images/1.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/2.png b/frontend/src/assets/images/2.png new file mode 100644 index 00000000..10928285 Binary files /dev/null and b/frontend/src/assets/images/2.png differ diff --git a/frontend/src/assets/images/2.svg b/frontend/src/assets/images/2.svg new file mode 100644 index 00000000..c6d68a0a --- /dev/null +++ b/frontend/src/assets/images/2.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/3.png b/frontend/src/assets/images/3.png new file mode 100644 index 00000000..b50694ec Binary files /dev/null and b/frontend/src/assets/images/3.png differ diff --git a/frontend/src/assets/images/3.svg b/frontend/src/assets/images/3.svg new file mode 100644 index 00000000..deb9324c --- /dev/null +++ b/frontend/src/assets/images/3.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/4.png b/frontend/src/assets/images/4.png new file mode 100644 index 00000000..9cfcc5ca Binary files /dev/null and b/frontend/src/assets/images/4.png differ diff --git a/frontend/src/assets/images/4.svg b/frontend/src/assets/images/4.svg new file mode 100644 index 00000000..1af15c80 --- /dev/null +++ b/frontend/src/assets/images/4.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/5.png b/frontend/src/assets/images/5.png new file mode 100644 index 00000000..af55c5f1 Binary files /dev/null and b/frontend/src/assets/images/5.png differ diff --git a/frontend/src/assets/images/5.svg b/frontend/src/assets/images/5.svg new file mode 100644 index 00000000..0775241f --- /dev/null +++ b/frontend/src/assets/images/5.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/6.png b/frontend/src/assets/images/6.png new file mode 100644 index 00000000..ac4189c4 Binary files /dev/null and b/frontend/src/assets/images/6.png differ diff --git a/frontend/src/assets/images/6.svg b/frontend/src/assets/images/6.svg new file mode 100644 index 00000000..ba8d2617 --- /dev/null +++ b/frontend/src/assets/images/6.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/ContentBanner.svg b/frontend/src/assets/images/ContentBanner.svg new file mode 100644 index 00000000..e6cf4dca --- /dev/null +++ b/frontend/src/assets/images/ContentBanner.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/comment-count.svg b/frontend/src/assets/images/comment-count.svg new file mode 100644 index 00000000..b68bb66b --- /dev/null +++ b/frontend/src/assets/images/comment-count.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/howToGoRepo.svg b/frontend/src/assets/images/howToGoRepo.svg new file mode 100644 index 00000000..721b7ec4 --- /dev/null +++ b/frontend/src/assets/images/howToGoRepo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/images/howToLogin.svg b/frontend/src/assets/images/howToLogin.svg new file mode 100644 index 00000000..69c2d8fb --- /dev/null +++ b/frontend/src/assets/images/howToLogin.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/images/howToReadDocs.svg b/frontend/src/assets/images/howToReadDocs.svg new file mode 100644 index 00000000..951e60ef --- /dev/null +++ b/frontend/src/assets/images/howToReadDocs.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/images/howToStart.svg b/frontend/src/assets/images/howToStart.svg new file mode 100644 index 00000000..56a5afee --- /dev/null +++ b/frontend/src/assets/images/howToStart.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/DashBoard/DashBoardMissionList/NoContent.tsx b/frontend/src/components/DashBoard/DashBoardMissionList/NoContent.tsx index 72c848b4..e42e4eaa 100644 --- a/frontend/src/components/DashBoard/DashBoardMissionList/NoContent.tsx +++ b/frontend/src/components/DashBoard/DashBoardMissionList/NoContent.tsx @@ -1,28 +1,46 @@ import * as S from './NoContent.styled'; import { useNavigate } from 'react-router-dom'; import { ROUTES } from '@/constants/routes'; +import Button from '@/components/common/Button/Button'; interface NoContentProps { - type: 'submitted' | 'inProgress'; + type: 'submitted' | 'inProgress' | 'comments'; } export default function NoContent({ type }: NoContentProps) { const navigate = useNavigate(); + const route = + type === 'inProgress' + ? ROUTES.missionList + : type === 'submitted' + ? ROUTES.solutions + : ROUTES.solutions; const handleNavigateToMissionList = () => { - navigate(ROUTES.missionList); + navigate(route); }; - const mainText = type === 'inProgress' ? '진행 중인 미션이 없어요' : '제출한 솔루션이 없어요'; + const mainText = + type === 'inProgress' + ? '진행 중인 미션이 없어요' + : type === 'submitted' + ? '제출한 솔루션이 없어요' + : '제출한 댓글이 없어요'; const subText = - type === 'inProgress' ? '새로운 미션을 찾으러 가볼까요?' : '새로운 미션을 찾아보러 가볼까요?'; + type === 'inProgress' + ? '새로운 미션을 찾으러 가볼까요?' + : type === 'submitted' + ? '참여할 수 있는 미션을 찾아보러 가볼까요?' + : '댓글을 달아볼까요?'; + const buttonText = type === 'comments' ? '솔루션 둘러보기' : '미션 둘러보기'; return ( {mainText} {subText} - 미션 둘러보기 + + {/* {buttonText} */} ); } diff --git a/frontend/src/components/DashBoard/DashBoardMissionList/index.tsx b/frontend/src/components/DashBoard/DashBoardMissionList/index.tsx index 367f395f..b1dbc0d2 100644 --- a/frontend/src/components/DashBoard/DashBoardMissionList/index.tsx +++ b/frontend/src/components/DashBoard/DashBoardMissionList/index.tsx @@ -21,7 +21,7 @@ export default function DashBoardMissionList({ missionList }: DashBoardMissionLi {mission.title} {mission.summary} - + ); diff --git a/frontend/src/components/DashBoard/MyComments/MyComment.tsx b/frontend/src/components/DashBoard/MyComments/MyComment.tsx new file mode 100644 index 00000000..0ac951b7 --- /dev/null +++ b/frontend/src/components/DashBoard/MyComments/MyComment.tsx @@ -0,0 +1,28 @@ +import type { MyComments } from '@/types'; +import { formatDateString } from '@/utils/formatDateString'; +import * as S from './MyComments.styled'; +import CommentIcon from '@/assets/images/comment-count.svg'; + +export default function MyComment({ + solutionId, + content, + createdAt, + solutionTitle, + solutionCommentCount, +}: Omit) { + const commentDate = formatDateString(createdAt); + + return ( + + + {content} + {commentDate} + {solutionTitle} + + + + {solutionCommentCount} + + + ); +} diff --git a/frontend/src/components/DashBoard/MyComments/MyCommentList.tsx b/frontend/src/components/DashBoard/MyComments/MyCommentList.tsx new file mode 100644 index 00000000..245c3d96 --- /dev/null +++ b/frontend/src/components/DashBoard/MyComments/MyCommentList.tsx @@ -0,0 +1,33 @@ +import type { MyComments } from '@/types'; +import * as S from './MyComments.styled'; +import NoContent from '../DashBoardMissionList/NoContent'; +import MyComment from './MyComment'; + +interface MyCommentListProps { + comments: MyComments[]; +} + +export default function MyCommentList({ comments }: MyCommentListProps) { + return ( + <> + {!comments.length ? ( + + ) : ( + + {comments.map((comment) => { + return ( + + ); + })} + + )} + + ); +} diff --git a/frontend/src/components/DashBoard/MyComments/MyComments.styled.ts b/frontend/src/components/DashBoard/MyComments/MyComments.styled.ts new file mode 100644 index 00000000..1dbb5646 --- /dev/null +++ b/frontend/src/components/DashBoard/MyComments/MyComments.styled.ts @@ -0,0 +1,49 @@ +import { styled } from 'styled-components'; +import { Link } from 'react-router-dom'; + +export const Container = styled.div` + width: 67.4rem; + height: 100%; +`; + +export const CommentWrapper = styled(Link)` + height: 8.5rem; + border-radius: 2.8rem; + background: ${(props) => props.theme.colors.white}; + box-shadow: ${(props) => props.theme.boxShadow.shadow04}; + padding: 14px 34px 14px 34px; + display: flex; + margin-top: 3rem; + margin-bottom: 1.5rem; + cursor: pointer; +`; + +export const CommentCountWrapper = styled.div` + margin-left: 2.8rem; + display: flex; + justify-content: center; + align-items: center; +`; + +export const TextWrapper = styled.div` + display: flex; + flex-direction: column; + min-width: 53rem; +`; + +export const CommentText = styled.span` + ${(props) => props.theme.font.body} + margin-bottom: 0.6rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const SubText = styled.span` + ${(props) => props.theme.font.badge} + color : var(--grey-400) +`; + +export const CommentCountText = styled(SubText)` + margin-left: 0.7rem; +`; diff --git a/frontend/src/components/Header/Header.styled.ts b/frontend/src/components/Header/Header.styled.ts index 5390b32a..fb254686 100644 --- a/frontend/src/components/Header/Header.styled.ts +++ b/frontend/src/components/Header/Header.styled.ts @@ -4,19 +4,26 @@ import styled from 'styled-components'; export const Container = styled.nav` z-index: 100; width: 100%; + min-width: 100rem; height: 6rem; position: fixed; - background: white; + background: ${(props) => props.theme.colors.white}; top: 0; left: 0; - box-shadow: - 2px 4px rgba(0, 0, 0, 0.08), - 0 4px 12px rgba(0, 0, 0, 0.08); + display: flex; + justify-content: center; + align-items: center; + + white-space: nowrap; +`; + +export const Wrapper = styled.div` + width: 100rem; display: flex; + flex-direction: row; justify-content: space-between; align-items: center; - padding: 0 26rem; `; export const LogoImg = styled.img` @@ -46,6 +53,12 @@ export const LeftPart = styled.div``; export const LoginButton = styled.button` ${(props) => props.theme.font.body} + padding: 0.5rem; + border-radius: 1rem; + + &:hover { + background-color: ${(props) => props.theme.colors.grey50}; + } `; export const MenuWrapper = styled.div` @@ -56,4 +69,10 @@ export const MenuWrapper = styled.div` export const MenuText = styled.p<{ $isActive?: boolean }>` ${(props) => props.theme.font.body} color: ${({ $isActive, theme }) => ($isActive ? '' : theme.colors.grey400)}; + padding: 0.5rem; + border-radius: 1rem; + + &:hover { + background-color: ${(props) => props.theme.colors.grey50}; + } `; diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index 24871c8d..a25ee893 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -1,47 +1,50 @@ import { Link, useLocation } from 'react-router-dom'; import * as S from './Header.styled'; import { ROUTES } from '@/constants/routes'; -import NotiModal from './NotiModal'; +// import NotiModal from './NotiModal'; import { BASE_URL } from '@/apis/baseUrl'; import { PATH } from '@/apis/paths'; import useUserInfo from '@/hooks/useUserInfo'; import HeaderMenu from './HeaderMenu'; import useLogoutMutation from '@/hooks/useLogoutMutation'; -import useModal from '@/hooks/useModal'; +// import useModal from '@/hooks/useModal'; export default function Header() { const { pathname } = useLocation(); const { data: userInfo } = useUserInfo(); const { handleUserLogout } = useLogoutMutation(); - const { isModalOpen, handleModalClose, handleToggleModal } = useModal(); + // const { isModalOpen, handleModalClose, handleToggleModal } = useModal(); return ( <> - - - 🚀 Devel Up - - - - - - - - {userInfo && } - {userInfo && ( - - )} - {!userInfo ? ( - - 로그인 - - ) : ( - 로그아웃 - )} - + + + + 🚀 DEVEL UP + + + + + + + + {/* 아직 알림이 mock data라서 주석처리 해놓겠습니다 @프룬 */} + {/* {userInfo && } */} + {userInfo && ( + + )} + {!userInfo ? ( + + 로그인 + + ) : ( + 로그아웃 + )} + + - {isModalOpen && } + {/* {isModalOpen && } */} ); diff --git a/frontend/src/components/MissionDetail/MissionDetail.styled.ts b/frontend/src/components/MissionDetail/MissionDetail.styled.ts index dd23ab39..6626f0a0 100644 --- a/frontend/src/components/MissionDetail/MissionDetail.styled.ts +++ b/frontend/src/components/MissionDetail/MissionDetail.styled.ts @@ -27,6 +27,7 @@ export const ThumbnailImg = styled.img` width: 100%; height: 100%; object-fit: cover; + object-position: 0 20%; `; export const GradientOverlay = styled.div` @@ -62,7 +63,6 @@ export const HashTagWrapper = styled.ul` export const MissionDetailButtonsContainer = styled.div` display: flex; - gap: 2rem; justify-content: space-between; align-items: center; `; @@ -71,6 +71,7 @@ export const InfoMsgWrapper = styled.div` display: flex; gap: 0.4rem; align-items: center; + justify-content: center; border-radius: 0.8rem; padding: 0.3rem; @@ -84,15 +85,13 @@ export const InfoMsgWrapper = styled.div` } `; -export const InfoIcon = styled(infoIcon)` - height: 100%; -`; +export const InfoIcon = styled(infoIcon)``; export const ButtonWrapper = styled.div` display: flex; gap: 0.8rem; width: 100%; - justify-content: center; + justify-content: space-between; `; export const GithubIcon = styled(githubLogo)` @@ -114,7 +113,6 @@ export const MissionDescription = styled.div` width: 100%; padding: 2rem; - background-color: ${(props) => props.theme.colors.grey50}; border-radius: 0.8rem; `; diff --git a/frontend/src/components/MissionDetail/MissionDetailButtons.tsx b/frontend/src/components/MissionDetail/MissionDetailButtons.tsx index afe9e6ca..0a3dd502 100644 --- a/frontend/src/components/MissionDetail/MissionDetailButtons.tsx +++ b/frontend/src/components/MissionDetail/MissionDetailButtons.tsx @@ -48,6 +48,11 @@ export default function MissionDetailButtons({ return ( + + {userInfo && !isMissionStarted && ( )} - - - - + + + 어떻게 진행하나요? + - - - 어떻게 참여하나요? - + + + ); } diff --git a/frontend/src/components/MissionList/MissionList.styled.ts b/frontend/src/components/MissionList/MissionList.styled.ts index c4e81ed8..287ff2b5 100644 --- a/frontend/src/components/MissionList/MissionList.styled.ts +++ b/frontend/src/components/MissionList/MissionList.styled.ts @@ -26,6 +26,8 @@ const show = keyframes` `; export const MissionList = styled.div` + flex-wrap: wrap; + display: flex; width: 100rem; column-gap: 5rem; diff --git a/frontend/src/components/MissionSubmit/MissionThumbnail.tsx b/frontend/src/components/MissionSubmit/MissionThumbnail.tsx index c4f28efa..770539a9 100644 --- a/frontend/src/components/MissionSubmit/MissionThumbnail.tsx +++ b/frontend/src/components/MissionSubmit/MissionThumbnail.tsx @@ -1,5 +1,5 @@ import * as S from './MissionThumbnail.styled'; -import Badge from '../common/Badge'; +// import Badge from '../common/Badge'; interface MissionImageProps { thumbnail: string; @@ -8,13 +8,15 @@ interface MissionImageProps { } export default function MissionImage({ thumbnail, title, language }: MissionImageProps) { + language; return ( {title} - + {/* 뱃지는 안쓰이고 있어서 주석처리할게요 @프룬 */} + {/* */} diff --git a/frontend/src/components/MissionSubmit/PRLink.tsx b/frontend/src/components/MissionSubmit/PRLink.tsx index a677b214..b8686d44 100644 --- a/frontend/src/components/MissionSubmit/PRLink.tsx +++ b/frontend/src/components/MissionSubmit/PRLink.tsx @@ -18,7 +18,7 @@ export default function PRLink({ missionId, value, onChange, danger }: PRLinkPro width="xlarge" danger={danger} dangerMessage={ERROR_MESSAGE.invalid_pr} - placeholder={`https://github.com/johndoe_dev/baseball-game/pull/${missionId}`} + placeholder={`https://github.com/develup-mission/baseball-game/pull/${missionId}`} value={value} onChange={onChange} /> diff --git a/frontend/src/components/MissionSubmit/SubmitBanner.styled.ts b/frontend/src/components/MissionSubmit/SubmitBanner.styled.ts index 87ba6b9d..f9d0aa80 100644 --- a/frontend/src/components/MissionSubmit/SubmitBanner.styled.ts +++ b/frontend/src/components/MissionSubmit/SubmitBanner.styled.ts @@ -13,7 +13,7 @@ export const GithubIcon = styled(GithubLogo)` height: 2.2rem; display: flex; justify-content: center; - margin-right: 0.3rem; + margin-right: 0.9rem; `; export const BannerTitle = styled.h1` diff --git a/frontend/src/components/MissionSubmit/SubmitButton.styled.ts b/frontend/src/components/MissionSubmit/SubmitButton.styled.ts index 1faf3a91..03a0296f 100644 --- a/frontend/src/components/MissionSubmit/SubmitButton.styled.ts +++ b/frontend/src/components/MissionSubmit/SubmitButton.styled.ts @@ -5,6 +5,8 @@ export const Container = styled.div` display: flex; justify-content: center; align-items: center; + + margin-bottom: 3rem; `; export const Button = styled.button` @@ -14,5 +16,9 @@ export const Button = styled.button` width: 100%; height: 5rem; ${(props) => props.theme.font.button} - margin-top: 10rem; + margin-top: 4rem; + + &:hover { + background: ${(props) => props.theme.colors.grey900}; + } `; diff --git a/frontend/src/components/ModalContent/MissionProcess/ModalProcess.styled.ts b/frontend/src/components/ModalContent/MissionProcess/MissionProcess.styled.ts similarity index 81% rename from frontend/src/components/ModalContent/MissionProcess/ModalProcess.styled.ts rename to frontend/src/components/ModalContent/MissionProcess/MissionProcess.styled.ts index f43fea24..234c804c 100644 --- a/frontend/src/components/ModalContent/MissionProcess/ModalProcess.styled.ts +++ b/frontend/src/components/ModalContent/MissionProcess/MissionProcess.styled.ts @@ -4,12 +4,11 @@ import Button from '@/components/common/Button/Button'; export const MissionProcessContentContainer = styled.div` width: 53rem; - height: 58rem; background: ${(props) => props.theme.colors.white}; box-shadow: ${(props) => props.theme.boxShadow.shadow08}; - border-radius: 0.5rem; + border-radius: 1.2rem; position: relative; - padding: 3.9rem; + padding: 7.8rem 5.6rem 4.5rem; display: flex; flex-direction: column; justify-content: center; @@ -17,14 +16,13 @@ export const MissionProcessContentContainer = styled.div` export const Title = styled.h1` ${(props) => props.theme.font.bodyBold} - margin-bottom: 2.4rem; `; export const CloseIconWrapper = styled.div` padding: 0.5rem; position: absolute; - top: 1rem; - right: 1rem; + top: 2.1rem; + right: 2.6rem; cursor: pointer; `; @@ -33,8 +31,10 @@ export const CloseIcon = styled(closeIcon)` height: 1.4rem; `; -export const Text = styled.p` - ${(props) => props.theme.font.body} +export const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 2.4rem; `; // Prev Next 버튼 @@ -42,7 +42,6 @@ export const Text = styled.p` export const ButtonWrapper = styled.div` display: flex; justify-content: center; - margin-top: 5rem; gap: 0.8rem; `; diff --git a/frontend/src/components/ModalContent/MissionProcess/index.tsx b/frontend/src/components/ModalContent/MissionProcess/index.tsx index 72b6dc20..e0b92e93 100644 --- a/frontend/src/components/ModalContent/MissionProcess/index.tsx +++ b/frontend/src/components/ModalContent/MissionProcess/index.tsx @@ -1,34 +1,57 @@ import { useState } from 'react'; -import * as S from './ModalProcess.styled'; +import * as S from './MissionProcess.styled'; import LeftArrow from '@/assets/images/smallLeftArrow.svg'; import RightArrow from '@/assets/images/smallRightArrow.svg'; -import ContentImage from '@/assets/images/contentImage.svg'; +import HowToLogin from '@/assets/images/howToLogin.svg'; +import HowToGoRepo from '@/assets/images/howToGoRepo.svg'; +import HowToReadDocs from '@/assets/images/howToReadDocs.svg'; +import HowToStart from '@/assets/images/howToStart.svg'; import { GithubIcon } from '@/components/MissionSubmit/SubmitBanner.styled'; import Button from '@/components/common/Button/Button'; +import ModalContent from '../index'; -const MOCK_CONTENT_LIST = [ +const CONTENT_LIST = [ { id: 1, - image: '', - content: '미션 저장소로 이동해주세요', + image: , + content: `우측 상단 **[로그인]** 버튼을 클릭해서 로그인을 수행해주세요.\n +로그인을 성공적으로 마치면 **[미션 시작하기]** 버튼을 클릭해서 미션을 시작할 수 있어요.`, + }, + { + id: 2, + image: , + content: `**[코드 보러 가기]** 버튼을 클릭해 풀고자하는 미션 저장소로 이동해주세요.`, + }, + { + id: 3, + image: , + content: `미션 저장소에서 **[미션 진행 가이드 문서]**를 확인할 수 있어요. 가이드 문서를 확인하여 코드를 작성해주세요.`, + }, + { + id: 4, + image: , + content: `미션 구현이 완료되면 **[풀이 제출하기]** 버튼을 클릭하여 풀이를 제출해주세요.\n +풀이 제출하기 버튼은 **[미션 시작하기]** 버튼을 누른 상태일때만 확인 가능해요.`, }, - { id: 2, image: '', content: '미션을 포크해주세요' }, - { id: 3, image: '', content: '미션을 진행해주세요' }, - { id: 4, image: '', content: '미션을 제출해주세요' }, ]; interface MissionProcessProps { handleModalClose: () => void; onClick: () => void; + onMission?: () => void; } -export default function MissionProcess({ handleModalClose, onClick }: MissionProcessProps) { +export default function MissionProcess({ + handleModalClose, + onClick, + onMission, +}: MissionProcessProps) { const [contentId, setContentId] = useState(1); - const currentContent = MOCK_CONTENT_LIST.find((content) => content.id === contentId); - const isEndContent = contentId === MOCK_CONTENT_LIST.length; + const currentContent = CONTENT_LIST.find((content) => content.id === contentId); + const isEndContent = contentId === CONTENT_LIST.length; const handleNextMissionProcess = () => { - setContentId((prev) => Math.min(prev + 1, MOCK_CONTENT_LIST.length)); + setContentId((prev) => Math.min(prev + 1, CONTENT_LIST.length)); }; const handlePreviousMissionProcess = () => { @@ -40,31 +63,40 @@ export default function MissionProcess({ handleModalClose, onClick }: MissionPro - 어떻게 진행하나요? - - {currentContent?.content} - - {isEndContent ? ( - <> - - - ) : ( - <> - - - Prev - + + 어떻게 진행하나요? + + + {isEndContent ? ( + <> + + {onMission && ( + + )} + + ) : ( + <> + + + Prev + - - Next - - - - )} - + + Next + + + + )} + + ); } diff --git a/frontend/src/components/ModalContent/ModalContent.styled.ts b/frontend/src/components/ModalContent/ModalContent.styled.ts new file mode 100644 index 00000000..000c2e2e --- /dev/null +++ b/frontend/src/components/ModalContent/ModalContent.styled.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +export const Text = styled.p` + ${(props) => props.theme.font.body} + white-space: pre-line; +`; + +export const BoldText = styled.span` + ${(props) => props.theme.font.bodyBold} +`; diff --git a/frontend/src/components/ModalContent/index.tsx b/frontend/src/components/ModalContent/index.tsx new file mode 100644 index 00000000..3b6bb662 --- /dev/null +++ b/frontend/src/components/ModalContent/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import * as S from './ModalContent.styled'; + +interface ModalContentProps { + contentImage: React.ReactNode; + content: string; +} + +export default function ModalContent({ contentImage, content }: ModalContentProps) { + return ( + <> + {contentImage} + + {content + .split('**') + .map((text, index) => + index % 2 === 1 ? {text} : text, + )} + + + ); +} diff --git a/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts b/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts index bcc68fee..e310b502 100644 --- a/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts +++ b/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts @@ -2,12 +2,12 @@ import SanitizedMDPreview from '@/components/common/SanitizedMDPreview'; import styled from 'styled-components'; export const CommentListContainer = styled.div` - margin-top: 3rem; + margin: 3rem 0; `; export const CommentItemContainer = styled.div` padding: 1.5rem 0; - border-bottom: 1px solid #c6b9ff; + border-bottom: 1px solid ${(props) => props.theme.colors.grey50}; &:first-child { padding-top: 0; @@ -20,8 +20,9 @@ export const CommentItemContainer = styled.div` `; export const CommentReplyItemContainer = styled.div` + padding: 3.2rem 0; padding: 1.5rem 0; - border-bottom: 1px solid ${({ theme }) => theme.colors.grey200}; + border-bottom: 1px solid ${({ theme }) => theme.colors.grey50}; &:first-child { padding-top: 0; diff --git a/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts b/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts index d0e66375..e84d6641 100644 --- a/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts +++ b/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts @@ -1,9 +1,15 @@ import styled from 'styled-components'; export const SolutionDetailPageContainer = styled.div` - padding: 0 22rem; + width: 100rem; + margin: 0 auto; `; export const CommentFormWrapper = styled.div` margin-top: 4rem; `; + +export const SeparationLine = styled.div` + border-top: solid 4px ${({ theme }) => theme.colors.grey200}; + margin: 10rem 0 4rem; +`; diff --git a/frontend/src/components/SolutionDetail/SolutionSection/SolutionDetailHeader.tsx b/frontend/src/components/SolutionDetail/SolutionSection/SolutionDetailHeader.tsx new file mode 100644 index 00000000..8108ac8f --- /dev/null +++ b/frontend/src/components/SolutionDetail/SolutionSection/SolutionDetailHeader.tsx @@ -0,0 +1,32 @@ +import type { Solution } from '@/types/solution'; +import * as S from './SolutionSection.styled'; +import HashTagButton from '@/components/common/HashTagButton'; + +interface SolutionDetailHeaderProps { + solution: Solution; +} + +export default function SolutionDetailHeader({ solution }: SolutionDetailHeaderProps) { + const { mission, member, title } = solution; + + return ( + + + + + + # {mission.title} + {title} + + + {member.name} + + + + {mission.hashTags && + mission.hashTags.map((tag) => # {tag.name})} + + + + ); +} diff --git a/frontend/src/components/SolutionDetail/SolutionSection/SolutionSection.styled.ts b/frontend/src/components/SolutionDetail/SolutionSection/SolutionSection.styled.ts new file mode 100644 index 00000000..61ab954b --- /dev/null +++ b/frontend/src/components/SolutionDetail/SolutionSection/SolutionSection.styled.ts @@ -0,0 +1,108 @@ +import styled from 'styled-components'; +import javaIcon from '@/assets/images/java.svg'; +import GithubLogo from '@/assets/images/githubLogo.svg'; + +export const SolutionDetailTitle = styled.h2` + margin: 4rem 0 2rem 0; + ${({ theme }) => theme.font.heading1} +`; + +export const MissionTitle = styled.div` + width: fit-content; + ${({ theme }) => theme.font.badge} + background-color: ${({ theme }) => theme.colors.danger50}; + padding: 1rem 2rem; + border-radius: 2rem; + + ${(props) => props.theme.font.badge} +`; + +export const HeaderUserName = styled.div` + color: ${({ theme }) => theme.colors.white}; + ${({ theme }) => theme.font.bodyBold} +`; + +export const HeaderUserInfo = styled.div` + display: flex; + align-items: center; + gap: 1.2rem; +`; + +export const SolutionDetailHeaderContainer = styled.div` + width: 100%; + height: 20rem; + margin: 0 auto; + display: flex; + flex-direction: column; + position: relative; +`; + +export const GithubIcon = styled(GithubLogo)` + width: 2.2rem; + height: 2.2rem; + display: flex; + justify-content: center; + margin-right: 0.3rem; +`; + +export const ThumbnailWrapper = styled.div` + position: relative; + height: 100%; + border-radius: 1rem; + overflow: hidden; +`; + +export const ThumbnailImg = styled.img` + width: 100%; + height: 100%; + object-fit: cover; +`; + +export const GradientOverlay = styled.div` + position: absolute; + inset: 0; + background: linear-gradient(rgba(0, 0, 0, 0), ${(props) => props.theme.colors.black}); + opacity: 0.5; + pointer-events: none; // 그라데이션이 클릭 이벤트를 방지하지 않도록 설정 +`; + +export const HeaderLeftArea = styled.div` + position: absolute; + left: 2.1rem; + bottom: 2.4rem; + display: flex; + flex-direction: column; +`; + +export const HeaderProfileImg = styled.img` + width: 4.2rem; + border-radius: 10rem; +`; + +export const Title = styled.h1` + margin: 1rem 0; + ${(props) => props.theme.font.heading1} + color: ${(props) => props.theme.colors.white}; +`; + +export const JavaIcon = styled(javaIcon)``; + +export const HashTagWrapper = styled.ul` + display: flex; + align-items: center; + justify-content: center; + gap: 1.1rem; + + position: absolute; + right: 2.1rem; + bottom: 2.4rem; +`; + +export const CodeViewButtonWrapper = styled.div` + margin: 3rem 0; +`; + +export const SolutionDescription = styled.div` + margin-top: 3rem; + ${({ theme }) => theme.font.body} +`; diff --git a/frontend/src/components/SolutionDetail/SolutionSection/index.tsx b/frontend/src/components/SolutionDetail/SolutionSection/index.tsx new file mode 100644 index 00000000..d60fb075 --- /dev/null +++ b/frontend/src/components/SolutionDetail/SolutionSection/index.tsx @@ -0,0 +1,29 @@ +import * as S from './SolutionSection.styled'; +import type { Solution } from '@/types/solution'; +import Button from '@/components/common/Button/Button'; +import SolutionDetailHeader from './SolutionDetailHeader'; +import { Link } from 'react-router-dom'; + +interface SolutionDetailProps { + solution: Solution; +} + +export default function SolutionSection({ solution }: SolutionDetailProps) { + const { description, url } = solution; + + return ( +
+ 📝 Solutions + + + + + + + {description} +
+ ); +} diff --git a/frontend/src/components/common/Input/Input.styled.ts b/frontend/src/components/common/Input/Input.styled.ts index fb5bb48f..404fcc8c 100644 --- a/frontend/src/components/common/Input/Input.styled.ts +++ b/frontend/src/components/common/Input/Input.styled.ts @@ -50,22 +50,23 @@ export const Input = styled.input` ${(props) => sizeStyles[props.$width]} ${(props) => borderRadiusStyles[props.$width]} ${(props) => typeStyles[props.$type]} + ${(props) => props.theme.font.body} outline: none; padding: 2.3rem; border: none; - border-bottom: 0.15rem solid transparent; - font-weight: bold; + border: 0.15rem solid transparent; + &::placeholder { color: rgba(0, 0, 0, 0.3); } ${(props) => props.$danger ? css` - border-bottom-color: ${(props) => props.theme.colors.danger600}; + border-color: ${(props) => props.theme.colors.danger600}; ` : css` &:focus { - border-bottom-color: ${(props) => props.theme.colors.primary500}; + border-color: ${(props) => props.theme.colors.primary500}; } `} `; diff --git a/frontend/src/components/common/TextArea/TextArea.styled.ts b/frontend/src/components/common/TextArea/TextArea.styled.ts index df0d9968..37f281b2 100644 --- a/frontend/src/components/common/TextArea/TextArea.styled.ts +++ b/frontend/src/components/common/TextArea/TextArea.styled.ts @@ -22,8 +22,8 @@ const typeStyles = { export const TextArea = styled.textarea` ${(props) => sizeStyles[props.$size]} ${(props) => typeStyles[props.$type]} + ${(props) => props.theme.font.body} outline: none; - font-weight: bold; padding: 1.5rem; min-height: 15rem; border-radius: 0.8rem; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index c60d2a0b..c04ebc38 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -11,4 +11,5 @@ export const ROUTES = { dashboardMissionInProgress: 'in-progress-mission', dashboardSubmittedSolution: 'submitted-solution', dashboardComments: 'comments', + about: '/about', } as const; diff --git a/frontend/src/hooks/queries/keys.ts b/frontend/src/hooks/queries/keys.ts index b5cef85e..54356d2a 100644 --- a/frontend/src/hooks/queries/keys.ts +++ b/frontend/src/hooks/queries/keys.ts @@ -12,10 +12,12 @@ export const missionKeys = { export const commentKeys = { all: (solutionId: number) => ['comments', solutionId], + mine: ['myComments'], }; export const solutionKeys = { all: ['solutions'], + detail: (id: number) => [...solutionKeys.all, id], summaries: ['solutionSummaries'], submitted: ['submitted solutions'], }; diff --git a/frontend/src/hooks/useMyComments.ts b/frontend/src/hooks/useMyComments.ts new file mode 100644 index 00000000..99c4f65f --- /dev/null +++ b/frontend/src/hooks/useMyComments.ts @@ -0,0 +1,12 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { commentKeys } from './queries/keys'; +import { getMyComments } from '@/apis/commentAPI'; + +const useMyComments = () => { + return useSuspenseQuery({ + queryKey: commentKeys.mine, + queryFn: getMyComments, + }); +}; + +export default useMyComments; diff --git a/frontend/src/hooks/useSolution.ts b/frontend/src/hooks/useSolution.ts new file mode 100644 index 00000000..9a5d034e --- /dev/null +++ b/frontend/src/hooks/useSolution.ts @@ -0,0 +1,13 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { solutionKeys } from './queries/keys'; +import type { Solution } from '@/types/solution'; +import { getSolutionById } from '@/apis/solutions'; + +const useSolution = (solutionId: number) => { + return useSuspenseQuery({ + queryKey: solutionKeys.detail(solutionId), + queryFn: () => getSolutionById(solutionId), + }); +}; + +export default useSolution; diff --git a/frontend/src/hooks/useSubmitSolution.ts b/frontend/src/hooks/useSubmitSolution.ts index fbc95803..d68d438d 100644 --- a/frontend/src/hooks/useSubmitSolution.ts +++ b/frontend/src/hooks/useSubmitSolution.ts @@ -4,16 +4,18 @@ import useSubmitSolutionMutation from './useSubmitSolutionMutation'; import useModal from './useModal'; import type { FormEvent } from 'react'; import useSolutionTitle from './useSolutionTitle'; -import extractMissionName from '@/utils/extractMissionName'; +// import extractMissionName from '@/utils/extractMissionName'; interface UseSubmitSolutionParams { missionId: number; missionName: string; } -const useSubmitSolution = ({ missionId, missionName }: UseSubmitSolutionParams) => { +const useSubmitSolution = ({ missionId }: UseSubmitSolutionParams) => { const { url, handleUrl, isValidUrl, isUrlError, setIsUrlError } = useUrl(); - const isMatchedMissionName = missionName === extractMissionName(url); + + //TODO 임시 주석 처리 + // const isMatchedMissionName = missionName === extractMissionName(url); const { description, @@ -69,7 +71,7 @@ const useSubmitSolution = ({ missionId, missionName }: UseSubmitSolutionParams) isUrlError, isDescriptionError, isSolutionTitleError, - isMatchedMissionName, + // isMatchedMissionName, }; }; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 099870d0..b6c0d63a 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -23,6 +23,8 @@ import DashBoardMissionInProgressPage from './pages/DashboardPage/MissionInProgr import SubmittedSolutionList from './components/DashBoard/SubmittedSolutions'; import { ThemeProvider } from 'styled-components'; import { theme } from './styles/theme'; +import MyCommentsPage from './pages/DashboardPage/MyComments'; +import AboutPage from './pages/AboutPage/AboutPage'; export const queryClient = new QueryClient({ defaultOptions: { @@ -150,7 +152,7 @@ const routes = [ path: ROUTES.dashboardComments, element: ( }> -
comments
+
), }, @@ -166,6 +168,14 @@ const routes = [ ), }, + { + path: ROUTES.about, + element: ( + + + + ), + }, ]; export const router = createBrowserRouter(routes, { @@ -190,8 +200,10 @@ export const router = createBrowserRouter(routes, { // // // 에러에요!}> -// -// +// +// +// +// // // // diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 3b8cd227..4f90745f 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -1,12 +1,14 @@ import { http, HttpResponse } from 'msw'; -import { BASE_URL } from '@/apis/baseUrl'; import { PATH } from '@/apis/paths'; import missions from './missions.json'; import submittedSolutions from './SubmittedSolutions.json'; +import myComments from './myComments.json'; +import missionInProgress from './missionInProgress.json'; import { HASHTAGS } from '@/constants/hashTags'; +import { API_URL } from '@/apis/clients/develupClient'; export const handlers = [ - http.get(`${BASE_URL.dev}${PATH.missionList}`, ({ request }) => { + http.get(`${API_URL}${PATH.missionList}`, ({ request }) => { const url = new URL(request.url); const hashTag = url.searchParams.get('hashTag'); if (hashTag === HASHTAGS.all) { @@ -18,7 +20,7 @@ export const handlers = [ return HttpResponse.json({ data: filteredMissions }); }), - http.get(`${BASE_URL.dev}${PATH.missionList}/:id`, ({ request }) => { + http.get(`${API_URL}${PATH.missionList}/:id`, ({ request }) => { const url = new URL(request.url); const id = Number(url.pathname.split('/').pop()); const mission = missions.find((mission) => mission.id === id); @@ -28,7 +30,7 @@ export const handlers = [ // return HttpResponse.json({ data: submission }); // }), - http.get(`${BASE_URL.dev}${PATH.userInfo}`, () => { + http.get(`${API_URL}${PATH.userInfo}`, () => { return HttpResponse.json( { data: { @@ -47,40 +49,17 @@ export const handlers = [ }, ); }), - http.get(`${BASE_URL.dev}${PATH.missionInProgress}`, () => { + http.get(`${API_URL}${PATH.missionInProgress}`, () => { return HttpResponse.json({ - data: [ - { - id: 1, - title: '루터회관 흡연 단속', - language: 'JAVA', - summary: '루터회관 흡연 벌칙 프로그램을 구현한다.', - thumbnail: - 'https://file.notion.so/f/f/d5a9d2d0-0fab-48ee-9e8a-13a13de1ac48/38a7f41b-80d7-48ca-97c9-99ceda5c4dbd/smoking.png?id=60756a7a-c50f-4946-ab6e-4177598b926b&table=block&spaceId=d5a9d2d0-0fab-48ee-9e8a-13a13de1ac48&expirationTimestamp=1721174400000&signature=todzUdb5cUyzW4ZQNaHvL-uiCngfMJJAl94RpE1TGEA&downloadName=smoking.png', - hashtag: ['java', 'backend', '문제', 'ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ'], - }, - { - id: 2, - title: '프론트엔드 숫자 야구 문제', - language: 'React', - summary: '프론트엔드 숫자 야구 문젤르 구현합니다', - thumbnail: - 'https://file.notion.so/f/f/d5a9d2d0-0fab-48ee-9e8a-13a13de1ac48/38a7f41b-80d7-48ca-97c9-99ceda5c4dbd/smoking.png?id=60756a7a-c50f-4946-ab6e-4177598b926b&table=block&spaceId=d5a9d2d0-0fab-48ee-9e8a-13a13de1ac48&expirationTimestamp=1721174400000&signature=todzUdb5cUyzW4ZQNaHvL-uiCngfMJJAl94RpE1TGEA&downloadName=smoking.png', - hashtag: ['react', 'javascript', 'frontend', 'ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ'], - }, - ], + data: missionInProgress, }); }), - http.get(`${BASE_URL.dev}${PATH.submitSolution}`, () => { + http.get(`${API_URL}${PATH.submitSolution}`, () => { return HttpResponse.json({ data: submittedSolutions, }); }), - // http.post(`${BASE_URL.dev}${PATH.logout}`, () => { - // return HttpResponse.json(null, { - // headers: { - // 'Set-Cookie': 'token=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/', - // }, - // }); - // }), + http.get(`${API_URL}${PATH.myComments}`, () => { + return HttpResponse.json({ data: myComments }); + }), ]; diff --git a/frontend/src/mocks/missionInProgress.json b/frontend/src/mocks/missionInProgress.json index 24501daa..84829c2a 100644 --- a/frontend/src/mocks/missionInProgress.json +++ b/frontend/src/mocks/missionInProgress.json @@ -5,7 +5,8 @@ "language": "JAVA", "summary": "루터회관 흡연 벌칙 프로그램을 구현한다.", "thumbnail": "https://www.arimetrics.com/wp-content/uploads/2020/01/mockup-1.png", - "hashTag": [ + "url": "abcd", + "hashTags": [ { "id": 1, "name": "JAVA" @@ -30,7 +31,8 @@ "language": "React", "summary": "프론트엔드 숫자 야구 문젤르 구현합니다", "thumbnail": "https://www.arimetrics.com/wp-content/uploads/2020/01/mockup-1.png", - "hashTag": [ + "url": "abcd", + "hashTags": [ { "id": 1, "name": "JAVA" diff --git a/frontend/src/mocks/myComments.json b/frontend/src/mocks/myComments.json new file mode 100644 index 00000000..a63a95ea --- /dev/null +++ b/frontend/src/mocks/myComments.json @@ -0,0 +1,18 @@ +[ + { + "id": 1, + "solutionId": 1, + "content": "dsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsadsavdsvcasdcsacsaㅍ", + "createdAt": "2024-08-21T03:50:48.544Z", + "solutionTitle": "string", + "solutionCommentCount": 0 + }, + { + "id": 2, + "solutionId": 2, + "content": "stridsacsacdsvdscsadcsadsadang", + "createdAt": "2024-08-23T03:50:48.544Z", + "solutionTitle": "string", + "solutionCommentCount": 0 + } +] diff --git a/frontend/src/pages/AboutPage/AboutPage.styled.ts b/frontend/src/pages/AboutPage/AboutPage.styled.ts new file mode 100644 index 00000000..f6ff874b --- /dev/null +++ b/frontend/src/pages/AboutPage/AboutPage.styled.ts @@ -0,0 +1,22 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin: 0 auto; +`; + +export const ButtonWrapper = styled.div` + position: relative; +`; + +export const Button = styled.button` + position: absolute; +`; + +export const Image = styled.img` + width: 100%; + height: 100%; +`; diff --git a/frontend/src/pages/AboutPage/AboutPage.tsx b/frontend/src/pages/AboutPage/AboutPage.tsx new file mode 100644 index 00000000..2563ef98 --- /dev/null +++ b/frontend/src/pages/AboutPage/AboutPage.tsx @@ -0,0 +1,58 @@ +import One from '@/assets/images/1.svg'; +import Two from '@/assets/images/2.svg'; +import Three from '@/assets/images/3.svg'; +import Four from '@/assets/images/4.svg'; +import Five from '@/assets/images/5.svg'; +import Six from '@/assets/images/6.svg'; +import * as S from './AboutPage.styled'; +import Button from '@/components/common/Button/Button'; +import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '@/constants/routes'; + +export default function AboutPage() { + const navigate = useNavigate(); + + const navigateToMissionList = () => { + navigate(ROUTES.missionList); + }; + + const navigateToMain = () => { + navigate(ROUTES.main); + }; + + return ( + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/pages/DashboardPage/MyComments/index.tsx b/frontend/src/pages/DashboardPage/MyComments/index.tsx new file mode 100644 index 00000000..8872cc56 --- /dev/null +++ b/frontend/src/pages/DashboardPage/MyComments/index.tsx @@ -0,0 +1,8 @@ +import useMyComments from '@/hooks/useMyComments'; +import MyCommentList from '@/components/DashBoard/MyComments/MyCommentList'; + +export default function MyCommentsPage() { + const { data: myComments } = useMyComments(); + + return ; +} diff --git a/frontend/src/pages/DashboardPage/index.tsx b/frontend/src/pages/DashboardPage/index.tsx index 8711b7a3..de4d717c 100644 --- a/frontend/src/pages/DashboardPage/index.tsx +++ b/frontend/src/pages/DashboardPage/index.tsx @@ -1,7 +1,14 @@ -import { Outlet } from 'react-router-dom'; +import { Outlet, useLocation, Navigate } from 'react-router-dom'; import DashboardPageLayout from './DashBoardPageLayout'; +import { ROUTES } from '@/constants/routes'; export default function DashboardPage() { + const location = useLocation(); + + if (location.pathname === '/dashboard') { + return ; + } + return ( diff --git a/frontend/src/pages/MainPage/MainPage.styled.tsx b/frontend/src/pages/MainPage/MainPage.styled.tsx index 095f1d82..fba5c9e3 100644 --- a/frontend/src/pages/MainPage/MainPage.styled.tsx +++ b/frontend/src/pages/MainPage/MainPage.styled.tsx @@ -3,7 +3,6 @@ import styled from 'styled-components'; export const MainPageContainer = styled.div` display: flex; flex-direction: column; - gap: 5rem; margin: 0 auto; margin-bottom: 10rem; padding-top: 6rem; @@ -12,5 +11,6 @@ export const MainPageContainer = styled.div` export const MissionListTitle = styled.h2` ${(props) => props.theme.font.heading1} + margin-top: 6rem; margin-bottom: 3rem; `; diff --git a/frontend/src/pages/MainPage/index.tsx b/frontend/src/pages/MainPage/index.tsx index b2c09bea..dae8da6f 100644 --- a/frontend/src/pages/MainPage/index.tsx +++ b/frontend/src/pages/MainPage/index.tsx @@ -1,12 +1,24 @@ import * as S from './MainPage.styled'; import useMissions from '@/hooks/useMissions'; import MissionList from '@/components/MissionList'; +import Carousel from '@/components/Carousel/Carousel'; +import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '@/constants/routes'; +import ContentBanner from '@/assets/images/ContentBanner.svg'; export default function MainPage() { const { data: missions } = useMissions(); + const navigate = useNavigate(); + + const navigateToAboutPage = () => { + navigate(ROUTES.about); + }; return ( + + + 새로운 미션에 참여해 보세요! diff --git a/frontend/src/pages/MissionListPage/MissionListPage.styled.ts b/frontend/src/pages/MissionListPage/MissionListPage.styled.ts index e3034333..5702f2e9 100644 --- a/frontend/src/pages/MissionListPage/MissionListPage.styled.ts +++ b/frontend/src/pages/MissionListPage/MissionListPage.styled.ts @@ -11,7 +11,6 @@ export const MissionListPageContainer = styled.div` `; export const MissionListTitle = styled.h2` - margin-bottom: 3.5rem; ${(props) => props.theme.font.heading1}; `; diff --git a/frontend/src/pages/MissionSubmitPage.tsx b/frontend/src/pages/MissionSubmitPage.tsx index 2e83c97e..fb296d35 100644 --- a/frontend/src/pages/MissionSubmitPage.tsx +++ b/frontend/src/pages/MissionSubmitPage.tsx @@ -31,7 +31,7 @@ export default function MissionSubmitPage() { isUrlError, isDescriptionError, isSolutionTitleError, - isMatchedMissionName, + // isMatchedMissionName, } = useSubmitSolution({ missionId, missionName }); return ( @@ -53,7 +53,8 @@ export default function MissionSubmitPage() { value={url} onChange={handleUrl} missionId={missionId} - danger={isUrlError || !isMatchedMissionName} + // danger={isUrlError || !isMatchedMissionName} + danger={isUrlError} /> 0; + const isLoggedIn = Boolean(userInfo); + return ( + + {(hasComment || isLoggedIn) && } - {userInfo && ( + {isLoggedIn && ( diff --git a/frontend/src/pages/SolutionListPage/SolutionListPage.styled.ts b/frontend/src/pages/SolutionListPage/SolutionListPage.styled.ts index be116522..83fdc4e1 100644 --- a/frontend/src/pages/SolutionListPage/SolutionListPage.styled.ts +++ b/frontend/src/pages/SolutionListPage/SolutionListPage.styled.ts @@ -1,16 +1,12 @@ import styled, { keyframes } from 'styled-components'; export const SolutionTitle = styled.h2` - margin-bottom: 3.5rem; ${(props) => props.theme.font.heading1} `; export const Subtitle = styled.p` - font-size: 1.6rem; - font-weight: 500; - font-family: inherit; - - color: var(--grey-500); + ${(props) => props.theme.font.body} + color: ${(props) => props.theme.colors.grey500}; `; export const TitleWrapper = styled.div` diff --git a/frontend/src/styles/GlobalLayout.tsx b/frontend/src/styles/GlobalLayout.tsx index 5a18dd3f..d165eea1 100644 --- a/frontend/src/styles/GlobalLayout.tsx +++ b/frontend/src/styles/GlobalLayout.tsx @@ -3,5 +3,5 @@ import styled from 'styled-components'; export const GlobalLayout = styled.div` height: auto; min-height: 100vh; - width: 100vw; + min-width: 100rem; `; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 603fcb51..0b6b959c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -60,3 +60,12 @@ export interface CommentReply { member: UserInfo; createdAt: string; } + +export interface MyComments { + id: number; + solutionId: number; + content: string; + createdAt: string; + solutionTitle: string; + solutionCommentCount: number; +} diff --git a/frontend/src/types/mission.ts b/frontend/src/types/mission.ts index ee92dfdb..6a2d6b37 100644 --- a/frontend/src/types/mission.ts +++ b/frontend/src/types/mission.ts @@ -2,8 +2,9 @@ import type { HashTag } from '.'; export interface MissionInProgress { id: number; - thumbnail: string; title: string; + thumbnail: string; summary: string; - hashTag: HashTag[]; + url: string; + hashTags: HashTag[]; } diff --git a/frontend/src/types/solution.ts b/frontend/src/types/solution.ts index 5d903b12..e22e0ec5 100644 --- a/frontend/src/types/solution.ts +++ b/frontend/src/types/solution.ts @@ -1,10 +1,4 @@ -interface Mission { - id: number; - title: string; - thumbnail: string; - url: string; - descriptionUrl: string; -} +import type { Mission } from '.'; interface Member { id: number; diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index e779f88d..334c4f71 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -44,7 +44,7 @@ module.exports = { }, output: { - filename: 'bundle.js', + filename: 'bundle.[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true, },