diff --git a/.github/codeowners b/.github/codeowners new file mode 100644 index 00000000..3ee3817b --- /dev/null +++ b/.github/codeowners @@ -0,0 +1 @@ +* @wonjunYou @5uhwann @KoSeonJe diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3f794489..bbcb4339 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,7 @@ -## πŸ”₯ μ—°κ΄€ 이슈 - -- close #이슈번호 - ## πŸš€ μž‘μ—… λ‚΄μš© ## πŸ€” κ³ λ―Όν–ˆλ˜ λ‚΄μš© -## πŸ’¬ 리뷰 쀑점사항 \ No newline at end of file +## πŸ’¬ 리뷰 쀑점사항 diff --git a/.github/workflows/dev-server-deployer.yml b/.github/workflows/dev-server-deployer.yml new file mode 100644 index 00000000..7af760d4 --- /dev/null +++ b/.github/workflows/dev-server-deployer.yml @@ -0,0 +1,82 @@ +name: Develop Server Deployer (CD) + +on: workflow_dispatch +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get Github Actions IP Addresses + id: publicip + run: | + response=$(curl -s canhazip.com) + echo "ip=$response" >> "$GITHUB_OUTPUT" + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} + aws-region: 'ap-northeast-2' + + - name: Add GitHub Actions IP + run: | + aws ec2 authorize-security-group-ingress \ + --group-id ${{ secrets.DEV_EC2_SECURITY_GROUP_ID }} \ + --protocol tcp \ + --port 22 \ + --cidr ${{ steps.publicip.outputs.ip }}/32 + + - name: Copy Docker Compose file to server + uses: appleboy/scp-action@master + with: + host: ${{ secrets.DEV_INSTANCE_HOST }} + username: ${{ secrets.DEV_INSTANCE_USERNAME }} + key: ${{ secrets.DEV_INSTANCE_KEY }} + source: "./compose-dev.yaml" + target: "~/app/docker" + timeout: 120s + overwrite: true + + - name: Install Docker if not present + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEV_INSTANCE_HOST }} + username: ${{ secrets.DEV_INSTANCE_USERNAME }} + key: ${{ secrets.DEV_INSTANCE_KEY }} + script: | + if ! command -v docker >/dev/null 2>&1; then + echo "Installing Docker..." + sudo apt-get update + sudo apt-get install -y docker.io + else + echo "Docker already installed." + fi + if ! command -v docker-compose >/dev/null 2>&1; then + echo "Installing Docker Compose..." + sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + else + echo "Docker Compose already installed." + fi + + - name: Run Docker Compose up + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.DEV_INSTANCE_HOST }} + username: ${{ secrets.DEV_INSTANCE_USERNAME }} + key: ${{ secrets.DEV_INSTANCE_KEY }} + script: | + echo "${{ secrets.DOCKER_PASSWORD }}" | sudo docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + sudo docker-compose -f ~/app/docker/compose-dev.yaml pull + sudo docker-compose -f ~/app/docker/compose-dev.yaml up -d --force-recreate + + - name: Remove GitHub Actions IP + run: | + aws ec2 revoke-security-group-ingress \ + --group-id ${{ secrets.DEV_EC2_SECURITY_GROUP_ID }} \ + --protocol tcp \ + --port 22 \ + --cidr ${{ steps.publicip.outputs.ip }}/32 diff --git a/.github/workflows/dev-server-integrator.yml b/.github/workflows/dev-server-integrator.yml new file mode 100644 index 00000000..1ff5b4ab --- /dev/null +++ b/.github/workflows/dev-server-integrator.yml @@ -0,0 +1,63 @@ +name: Develop Server Integrator (CI) + +on: + push: + branches: + - develop + +jobs: + build_and_push: + runs-on: ubuntu-latest + steps: + - name: Check Out Repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: CI Test + run: ./gradlew clean test + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR Public + id: login-ecr-public + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + + - name: Build, tag, and push docker image to Amazon ECR Public + env: + REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} + REGISTRY_ALIAS: ${{ secrets.DEV_ECR_REGISTRY_ALIAS }} + REPOSITORY: dev-ecr + IMAGE_TAG: latest + run: | + echo "REPOSITORY: $REPOSITORY" + docker build -t $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG . + docker push $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG + echo "::set-output name=image::$REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG" + + - name: Logout of Amazon ECR + run: docker logout ${{ env.ECR_REGISTRY }} diff --git a/.github/workflows/dn-rule.yml b/.github/workflows/dn-rule.yml new file mode 100644 index 00000000..403b55cb --- /dev/null +++ b/.github/workflows/dn-rule.yml @@ -0,0 +1,105 @@ +name: PR Label Automation +on: + schedule: + - cron: '0 10 * * *' + +jobs: + update-labels: + runs-on: ubuntu-latest + steps: + - name: Check and Update PR Labels + uses: actions/github-script@v5 + with: + script: | + const repo = context.repo; + + // Fetch all open PRs + const prs = await github.rest.pulls.list({ + owner: repo.owner, + repo: repo.repo, + state: 'open', + }); + + // Define the Discord webhook URL + const webhookUrl = 'https://discord.com/api/webhooks/1273159249672802304/vU5b6gC9bAHzyLXzWRzV7YqjIVCtO5_gLJ1URonjnbqn45Xa5kixYT1vMWxwLXqFi2y3'; + + for (const pr of prs.data) { + const prNumber = pr.number; + let labels = pr.labels.map(label => label.name); + + // Function to update label + async function updateLabel(oldLabel, newLabel) { + if (oldLabel) { + await github.rest.issues.removeLabel({ + owner: repo.owner, + repo: repo.repo, + issue_number: prNumber, + name: oldLabel, + }); + } + await github.rest.issues.addLabels({ + owner: repo.owner, + repo: repo.repo, + issue_number: prNumber, + labels: [newLabel], + }); + } + + // Check and update 'D-x' labels + let dLabel = labels.find(label => label.startsWith("D-")); + if (dLabel) { + let day = parseInt(dLabel.split("-")[1]); + if (day > 0) { + const newDayLabel = `D-${day - 1}`; + await updateLabel(dLabel, newDayLabel); + console.log(`Updated label from ${dLabel} to ${newDayLabel} on PR #${prNumber}`); + + // Send a notification to Discord + await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: 'PR Bot', + avatar_url: 'https://avatars.githubusercontent.com/u/9919?s=200&v=4', + embeds: [ + { + title: `πŸ“’ **PR #${prNumber} - ${pr.title} Review D-${day - 1}** πŸ“’`, + description: `리뷰λ₯Ό μž‘μ„±ν•΄μ£Όμ„Έμš”! 리뷰 μž‘μ„± 기간이 ${day - 1}일 λ‚¨μ•˜μŠ΅λ‹ˆλ‹€.`, + url: pr.html_url, + color: 4620980, + footer: { + text: `D-dayκ°€ μ—…λ°μ΄νŠΈ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ${dLabel} β†’ ${newDayLabel}` + } + } + ] + }) + }); + } else if (day === 0) { + await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: 'PR Bot', + avatar_url: 'https://avatars.githubusercontent.com/u/9919?s=200&v=4', + embeds: [ + { + title: `❗ **PR #${prNumber} - ${pr.title} Review D-0** ❗`, + description: `리뷰 마감일이 μ§€λ‚¬μŠ΅λ‹ˆλ‹€! 리뷰λ₯Ό μž‘μ„±ν•΄μ£Όμ„Έμš”.`, + url: pr.html_url, + color: 4620980, + footer: { + text: `D-day μƒνƒœ: D-0` + } + } + ] + }) + }); + } + } else { + await updateLabel(null, 'D-3'); + } + } diff --git a/.github/workflows/ddingdong-dev-deploy.yml b/.github/workflows/prod-server-deployer.yml similarity index 97% rename from .github/workflows/ddingdong-dev-deploy.yml rename to .github/workflows/prod-server-deployer.yml index 4e0ab425..f7abfcbe 100644 --- a/.github/workflows/ddingdong-dev-deploy.yml +++ b/.github/workflows/prod-server-deployer.yml @@ -1,17 +1,16 @@ -name: ddingdong-dev-depoly +name: prod-server-deployer on: push: branches: - main - jobs: build: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v1 @@ -73,7 +72,6 @@ jobs: - name: Test with Gradle run: ./gradlew test --no-daemon - - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f959ac53 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# Build stage +FROM gradle:jdk17 AS build +COPY --chown=gradle:gradle . /home/gradle/src +WORKDIR /home/gradle/src +RUN ./gradlew clean build -x test --no-daemon + +# Package stage +FROM openjdk:17 +COPY --from=build /home/gradle/src/build/libs/*.jar /app.jar +EXPOSE 8080 +ENTRYPOINT ["java","-jar","/app.jar"] diff --git a/README.md b/README.md index 661e3e3f..c612acb4 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,173 @@ -# λͺ…μ§€λŒ€ν•™κ΅ 동아리 관리 μ‹œμŠ€ν…œ - ddingdong - -### URL -DEFAULT : https://ddingdong.club/ -ADMIN : https://admin.ddingdong.club/ - -### 기술 μŠ€νƒ - -

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

+
+ +# 띡동. + +**λͺ…μ§€λŒ€ν•™κ΅ 동아리 톡합 ν”Œλž«νΌ** + +"띡동"은 학생듀이 νŒŒνŽΈν™”λœ 동아리정보와 λΉ„νš¨μœ¨μ μΈ 동아리 업무 μ²˜λ¦¬κ³Όμ •μ„ μΌμ›ν™”ν•˜μ—¬ μ œκ³΅ν•˜λŠ” μ„œλΉ„μŠ€ μž…λ‹ˆλ‹€. + +### πŸŽƒ 쑰회수 & μ‚¬μš©μž 톡계(2024.08.09 κΈ°μ€€) + + + +### πŸ₯‡ ꡐ내 SW κ²½μ§„λŒ€νšŒ μš°μˆ˜μƒ, +### πŸ’™ λͺ…μ§€λŒ€ν•™κ΅ 곡식 동아리 μ›Ήμ‚¬μ΄νŠΈ 등둝 + +
+ +### 띡동 κΈ°λŠ₯ λͺ©λ‘ + +#### 동아리 κ°„νŽΈ 쑰회 + +![동아리 κ°„νŽΈ 쑰회](https://github.com/user-attachments/assets/1c7abd5f-c43b-4214-b26e-bdbb6dbff71f) + +- ν•„ν„°κΈ°λŠ₯(λͺ¨μ§‘κΈ°μ€€, μ •λ ¬ 방식, μΉ΄ν…Œκ³ λ¦¬)을 톡해 μ‚¬μš©μžκ°€ μ›ν•˜λŠ” 동아리λ₯Ό 검색할 수 μžˆμŠ΅λ‹ˆλ‹€. +- λͺ…μ§€λŒ€ν•™κ΅ 전체 λ™μ•„λ¦¬μ˜ 정보λ₯Ό ν•œ κ³΅κ°„μ—μ„œ μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- 검색을 톡해 μ›ν•˜λŠ” 동아리λ₯Ό μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- λ°°λ„ˆλ₯Ό 톡해 동아리 μ£Όμš”μ†Œμ‹μ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. + +#### 동아리 정보 쑰회 + +![동아리 정보](https://github.com/user-attachments/assets/06764ca8-5efb-42b6-ba8e-fc86aefc8a10) + +- 동아리가 κ΄€λ¦¬ν•˜λŠ” 정보λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. +- 각 λ™μ•„λ¦¬μ˜ λͺ¨μ§‘κΈ°κ°„μ—λŠ” κ°„νŽΈν•˜κ²Œ 동아리 μ§€μ›μœΌλ‘œ μ—°κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- 각 λ™μ•„λ¦¬λŠ” 동아리 μ†Œκ°œ λ‚΄μš©μ„ μˆ˜μ • 및 관리할 수 μžˆμŠ΅λ‹ˆλ‹€. + +#### 곡지사항 + +![곡지사항](https://github.com/user-attachments/assets/56919552-6ff3-4938-a4ad-68651537e871) + +- 곡지사항을 톡해 동아리 정보λ₯Ό μ‰½κ²Œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. +- κ³΅μ§€μ‚¬ν•­μ˜ 첨뢀 νŒŒμΌμ„ 클릭 μ‹œ, λ‹€μš΄ 받을 수 μžˆμŠ΅λ‹ˆλ‹€. + +### μ–΄λ“œλ―Ό κΈ°λŠ₯ λͺ©λ‘ + +#### 동아리 관리 + +![동아리 관리](https://github.com/user-attachments/assets/79071858-6fbe-4aa4-a5f8-076e2dbfe21f) + +- 총동아리 μ—°ν•©νšŒλŠ” 동아리λ₯Ό 생성 및 μ‚­μ œν•  수 μžˆλŠ” κΆŒν•œμ„ κ°–μŠ΅λ‹ˆλ‹€. +- 총동아리 μ—°ν•©νšŒλŠ” 각 λ™μ•„λ¦¬μ—κ²Œ 점수λ₯Ό λΆ€μ—¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- 각 λ™μ•„λ¦¬λŠ” 동아리에 λΆ€μ—¬λ˜λŠ” κ³ μœ ν•œ 아이디와, λΉ„λ°€λ²ˆν˜Έλ₯Ό 톡해 λ‘œκ·ΈμΈν•˜μ—¬ 동아리 관리 νŽ˜μ΄μ§€μ— μ ‘κ·Όν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +#### 동아리원 λͺ…단 관리 + +![동아리원](https://github.com/user-attachments/assets/43d430ed-6489-4084-80c2-bbcfe97b283f) + +- excel을 ν†΅ν•œ 동아리원 일괄 μ—…λ‘œλ“œ κΈ°λŠ₯을 μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. +- 직접 μˆ˜μ • κΈ°λŠ₯을 톡해 동아리 μ›μ˜ 정보λ₯Ό μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- 검색 κΈ°λŠ₯을 톡해 νŠΉμ • ν•™κ³Όλ‚˜ νŠΉμ • 뢀원을 μ‰½κ²Œ 필터링할 수 μžˆμŠ΅λ‹ˆλ‹€. + +#### ν™œλ™λ³΄κ³ μ„œ + +![ν™œλ™λ³΄κ³ μ„œ](https://github.com/user-attachments/assets/5586cf9c-e02d-43f3-9e9f-3a4ecd76a566) + +- 총동아리 μ—°ν•©νšŒλŠ” 주차별 λ™μ•„λ¦¬μ˜ ν™œλ™ λ³΄κ³ μ„œ λͺ©λ‘μ„ μ‘°νšŒν•  수 있으며, ν•„ν„°(μ œμΆœμ™„λ£Œ, 미제좜, 전체)κΈ°λŠ₯을 톡해 ν¬λ§ν•˜λŠ” λ³΄κ³ μ„œλ§Œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. +- 각 λ™μ•„λ¦¬λŠ” ν˜„μž¬ λ‚ μ§œμ— ν•΄λ‹Ήν•˜λŠ” 회차의 ν™œλ™λ³΄κ³ μ„œλ§Œμ„ μž‘μ„±ν•  수 있으며, μˆ˜μ • 및 μ‚­μ œ κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. +- 뢀원을 μž‘μ„±ν•˜λŠ” κ²½μš°μ—λŠ” λ΅λ™μ˜ 동아리 뢀원 데이터λ₯Ό μ—°λ™ν•˜λ―€λ‘œ 이름 μž…λ ₯만으둜, ν•™λ²ˆ 및 ν•™κ³Ό 데이터λ₯Ό 뢈러올 수 μžˆμŠ΅λ‹ˆλ‹€. + +#### μ‹œμ„€λ³΄μˆ˜ + +![ν”½μŠ€μ‘΄](https://github.com/user-attachments/assets/24a135e5-1eb8-45d5-ae85-1f9915e9f827) + +- 각 λ™μ•„λ¦¬λŠ” μ‹œμ„€λ³΄μˆ˜ κΈ°λŠ₯을 톡해 동아리 방의 μ‹œμ„€λ³΄μˆ˜ μš”μ²­κ±΄μ„ nμž₯의 사진, 제λͺ©, μ„€λͺ…을 톡해 μš”μ²­ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- 총 동아리 μ—°ν•©νšŒλŠ” μ‹œμ„€λ³΄μˆ˜λ₯Ό 톡해 처리 μ—¬λΆ€(μ²˜λ¦¬μ€‘/μ²˜λ¦¬μ™„λ£Œ)λ₯Ό 전달할 수 μžˆμŠ΅λ‹ˆλ‹€. +- 총 동아리 μ—°ν•©νšŒλŠ” μ‹œμ„€λ³΄μˆ˜ μš”μ²­κ±΄μ— λŒ€ν•΄ λŒ“κΈ€μ„ 생성할 수 μžˆμŠ΅λ‹ˆλ‹€. + +#### 곡지사항 + +![곡지사항 μž‘μ„±](https://github.com/user-attachments/assets/f9a2e2d2-9a50-4953-9b8a-1777f62dd26d) + +- 총동아리 μ—°ν•©νšŒλŠ” 파일과 사진을 λ“±λ‘ν•˜μ—¬ 곡지사항을 생성할 수 μžˆμŠ΅λ‹ˆλ‹€. +- ν•΄λ‹Ή 곡지사항은 각 동아리 회μž₯ 뿐만 μ•„λ‹ˆλΌ 일반 ν•™μš°κΉŒμ§€ 전체 μ‚¬μš©μžμ—κ²Œ λ…ΈμΆœλ©λ‹ˆλ‹€. + +#### λ°°λ„ˆκ΄€λ¦¬ + +![λ°°λ„ˆ](https://github.com/user-attachments/assets/5904477a-a5d2-49d0-8e6e-e96c0b373119) + +- 총동아리 μ—°ν•©νšŒλŠ” λ΅λ™μ˜ λ°°λ„ˆλ₯Ό 생성, μˆ˜μ • 그리고 μ‚­μ œ κΈ°λŠ₯을 톡해 관리할 수 μžˆμŠ΅λ‹ˆλ‹€. +- 이미지, 메인 문ꡬ, μ†Œκ°œ 문ꡬ, 색상을 μž…λ ₯ν•˜μ—¬ ν†΅μΌλœ λ°°λ„ˆ 이미지λ₯Ό μ—°μΆœν•  수 있으며, 홍보등에 μ‚¬μš©λ©λ‹ˆλ‹€. +- ν•΄λ‹Ή λ°°λ„ˆλŠ” 일반 μ„œλΉ„μŠ€μ™€ μ–΄λ“œλ―Ό μ„œλΉ„μŠ€μ— λͺ¨λ‘ μ μš©λ©λ‹ˆλ‹€. + +#### 동아리 점수 확인 + +![동아리 점수 확인](https://github.com/user-attachments/assets/2062b02b-7fa2-416c-9578-7fd779e8622e) + +- μΉ΄ν…Œκ³ λ¦¬λ³„ 점수λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. +- 동아리 μ μˆ˜κ°€ λΆ€μ—¬λœ λ‚΄μ—­(λ‚ μ§œ, μΉ΄ν…Œκ³ λ¦¬, 점수)리슀트λ₯Ό μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +## 🀟 λ΅λ™νŒ€ μ†Œκ°œ + +### Front-End + + + + + + + + + + + + + + +
+ + + + + + + +
κΉ€μ„ΈλΉˆλͺ¨μœ κ²½
FE DeveloperFE Developer
+ +### Back-End + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
κ³ μ„ μ œλ°•μˆ˜ν™˜μœ μ›μ€€
BE DeveloperBE DeveloperBE Developer
+ +### Designer + + + + + + + + + + + +
+ +
λ΄‰μ„œμ—°
Designer
diff --git a/build.gradle b/build.gradle index a775e2e5..150a163e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,21 @@ +buildscript { + ext { + queryDslVersion = '5.0.0' + } +} + plugins { id 'java' id 'org.springframework.boot' version '2.7.12' id 'io.spring.dependency-management' version '1.0.15.RELEASE' - id 'jacoco' + id 'org.jetbrains.kotlin.jvm' + id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } group = 'ddingdong' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' } jar { @@ -27,63 +33,81 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + //develop implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springdoc:springdoc-openapi-ui:1.6.11' + implementation 'org.springframework.boot:spring-boot-configuration-processor' - implementation 'io.hypersistence:hypersistence-utils-hibernate-55:3.7.2' - - implementation 'com.auth0:java-jwt:4.2.1' + implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" + implementation "com.querydsl:querydsl-apt:${queryDslVersion}" - implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4' + //db + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'com.mysql:mysql-connector-j' + implementation 'org.flywaydb:flyway-core' + implementation "org.flywaydb:flyway-mysql" + //etc(기타) + implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4' implementation 'org.apache.poi:poi:5.2.0' implementation 'org.apache.poi:poi-ooxml:5.2.0' - implementation 'org.springframework.boot:spring-boot-configuration-processor' - + implementation 'io.hypersistence:hypersistence-utils-hibernate-55:3.7.2' implementation 'io.sentry:sentry-logback:7.6.0' - implementation 'org.springdoc:springdoc-openapi-ui:1.6.11' + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.github.f4b6a3:uuid-creator:6.0.0' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2' - annotationProcessor 'org.projectlombok:lombok' + //security + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'com.auth0:java-jwt:4.2.1' + + //test testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + testRuntimeOnly "org.junit.platform:junit-platform-launcher" + testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.23") + // TestContainer + testImplementation 'org.testcontainers:testcontainers:1.20.1' + testImplementation 'org.testcontainers:junit-jupiter:1.20.1' + // mysql μ»¨ν…Œμ΄λ„ˆ + testImplementation 'org.testcontainers:mysql:1.20.1' } tasks.named('test') { useJUnitPlatform() } +def querydslDir = "$buildDir/generated/'querydsl'" -jacoco { - toolVersion = '0.8.10' +querydsl { // JPA μ‚¬μš©μ—¬λΆ€ 및 μ‚¬μš© 경둜 μ„€μ • + jpa = true + querydslSourcesDir = querydslDir } -test { - finalizedBy jacocoTestReport +sourceSets { // buildμ‹œ μ‚¬μš©ν•  sourceSet μΆ”κ°€ μ„€μ • + main.java.srcDir querydslDir } -jacocoTestReport { - reports { - html.enabled true - xml.enabled true - csv.enabled true - } +compileQuerydsl { // querydsl 컴파일 μ‹œ μ‚¬μš©ν•  μ˜΅μ…˜ μ„€μ • + options.annotationProcessorPath = configurations.querydsl } -jacoco { - toolVersion = '0.8.10' +// querydsl이 compileClassPathλ₯Ό μƒμ†ν•˜λ„λ‘ μ„€μ • +configurations { + compileOnly { + extendsFrom annotationProcessor + } + querydsl.extendsFrom compileClasspath } -test { - finalizedBy jacocoTestReport +compileQuerydsl.doFirst { + if (file(querydslDir).exists()) + delete(file(querydslDir)) } -jacocoTestReport { - reports { - html.enabled true - xml.enabled true - csv.enabled true - } -} \ No newline at end of file +compileQuerydsl { + options.annotationProcessorPath = configurations.querydsl +} diff --git a/compose-dev.yaml b/compose-dev.yaml new file mode 100644 index 00000000..eabd2f89 --- /dev/null +++ b/compose-dev.yaml @@ -0,0 +1,27 @@ +version: '3.8' +services: + spring: + image: public.ecr.aws/${DEV_ECR_REGISTRY_ALIAS}/dev-ecr:${VERSION:-latest} + volumes: + - mysql-volume:/var/lib/mysql + environment: + - VERSION=${VERSION:-latest} + - SPRING_PROFILES_ACTIVE=dev + pull_policy: always + env_file: + - .env + depends_on: + - mysql + ports: + - "8080:8080" + mysql: + image: mysql:8.0.33 + environment: + MYSQL_DATABASE: ddingdong + MYSQL_ROOT_PASSWORD: ${DEV_DB_PASSWORD} + TZ: Asia/Seoul + ports: + - "3306:3306" + +volumes: + mysql-volume: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..e10c549a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + ddingdong-local-db: + image: mysql:8.0 + container_name: ddingdong_local_mysql + platform: linux/x86_64 + environment: # ν™˜κ²½ λ³€μˆ˜ μ„€μ • + MYSQL_ROOT_PASSWORD: 1234 + MYSQL_DATABASE: ddingdong_local_db + MYSQL_CHARSET: utf8mb4 + MYSQL_COLLATION: utf8mb4_unicode_ci + TZ: Asia/Seoul + ports: + - "3307:3306" + volumes: + - backup-store:/var/lib/mysql +volumes: + backup-store : \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 6855ab65..00839d8b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,9 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version '2.0.0' + } +} +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} rootProject.name = 'ddingdong-BE' diff --git a/src/main/java/ddingdong/ddingdongBE/DdingdongBeApplication.java b/src/main/java/ddingdong/ddingdongBE/DdingdongBeApplication.java index 4b815d8f..1dfc7d36 100644 --- a/src/main/java/ddingdong/ddingdongBE/DdingdongBeApplication.java +++ b/src/main/java/ddingdong/ddingdongBE/DdingdongBeApplication.java @@ -1,11 +1,18 @@ package ddingdong.ddingdongBE; +import java.util.TimeZone; +import javax.annotation.PostConstruct; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DdingdongBeApplication { + @PostConstruct + public void started() { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + } + public static void main(String[] args) { SpringApplication.run(DdingdongBeApplication.class, args); } diff --git a/src/main/java/ddingdong/ddingdongBE/auth/PrincipalDetails.java b/src/main/java/ddingdong/ddingdongBE/auth/PrincipalDetails.java index c8c7c616..c77a65d0 100644 --- a/src/main/java/ddingdong/ddingdongBE/auth/PrincipalDetails.java +++ b/src/main/java/ddingdong/ddingdongBE/auth/PrincipalDetails.java @@ -1,6 +1,7 @@ package ddingdong.ddingdongBE.auth; import ddingdong.ddingdongBE.domain.user.entity.User; +import io.swagger.v3.oas.annotations.Hidden; import java.util.ArrayList; import java.util.Collection; import lombok.Getter; @@ -9,6 +10,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +@Hidden @RequiredArgsConstructor @Getter public class PrincipalDetails implements UserDetails { diff --git a/src/main/java/ddingdong/ddingdongBE/auth/service/JwtAuthService.java b/src/main/java/ddingdong/ddingdongBE/auth/service/JwtAuthService.java index d2899529..dfd6e9a8 100644 --- a/src/main/java/ddingdong/ddingdongBE/auth/service/JwtAuthService.java +++ b/src/main/java/ddingdong/ddingdongBE/auth/service/JwtAuthService.java @@ -1,15 +1,16 @@ package ddingdong.ddingdongBE.auth.service; -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; - import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.JWTVerifier; import ddingdong.ddingdongBE.auth.PrincipalDetails; import ddingdong.ddingdongBE.auth.controller.dto.request.SignInRequest; import ddingdong.ddingdongBE.common.config.JwtConfig; -import ddingdong.ddingdongBE.common.exception.AuthenticationException; +import ddingdong.ddingdongBE.common.exception.AuthenticationException.InvalidPassword; +import ddingdong.ddingdongBE.common.exception.AuthenticationException.NonExistUserRole; +import ddingdong.ddingdongBE.common.exception.AuthenticationException.UnRegisteredId; +import ddingdong.ddingdongBE.common.exception.RegisterClubException.AlreadyExistClubId; import ddingdong.ddingdongBE.domain.user.entity.Password; import ddingdong.ddingdongBE.domain.user.entity.Role; import ddingdong.ddingdongBE.domain.user.entity.User; @@ -28,7 +29,7 @@ @Service @Transactional @RequiredArgsConstructor -public class JwtAuthService implements AuthService{ +public class JwtAuthService implements AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @@ -50,10 +51,10 @@ public User registerClubUser(String userId, String password, String name) { @Override public String signIn(SignInRequest request) { User user = userRepository.findByUserId(request.getUserId()) - .orElseThrow(() -> new AuthenticationException(UNREGISTER_ID)); + .orElseThrow(UnRegisteredId::new); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { - throw new AuthenticationException(INVALID_PASSWORD); + throw new InvalidPassword(); } PrincipalDetails principalDetails = new PrincipalDetails(user); @@ -74,7 +75,7 @@ public String getUserRole() { return authorities.stream() .findFirst() - .orElseThrow(() -> new IllegalArgumentException("USER_ROLE이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")) + .orElseThrow(NonExistUserRole::new) .getAuthority(); } @@ -114,7 +115,7 @@ private String createJwt(User user) { private void checkExistUserId(String userId) { if (userRepository.existsByUserId(userId)) { - throw new IllegalArgumentException(ALREADY_EXIST_CLUB_ID.getText()); + throw new AlreadyExistClubId(); } } } diff --git a/src/main/java/ddingdong/ddingdongBE/common/BaseEntity.java b/src/main/java/ddingdong/ddingdongBE/common/BaseEntity.java index 4f69eb1e..a418273d 100644 --- a/src/main/java/ddingdong/ddingdongBE/common/BaseEntity.java +++ b/src/main/java/ddingdong/ddingdongBE/common/BaseEntity.java @@ -13,13 +13,19 @@ @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { - @CreatedDate - @Column(columnDefinition = "TIMESTAMP") + @Column(columnDefinition = "TIMESTAMP", updatable = false) private LocalDateTime createdAt; @LastModifiedDate @Column(columnDefinition = "TIMESTAMP") private LocalDateTime updatedAt; + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } } diff --git a/src/main/java/ddingdong/ddingdongBE/common/config/QueryDslConfig.java b/src/main/java/ddingdong/ddingdongBE/common/config/QueryDslConfig.java new file mode 100644 index 00000000..4ce600c7 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package ddingdong.ddingdongBE.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/config/SecurityConfig.java b/src/main/java/ddingdong/ddingdongBE/common/config/SecurityConfig.java index b384952e..cbee601a 100644 --- a/src/main/java/ddingdong/ddingdongBE/common/config/SecurityConfig.java +++ b/src/main/java/ddingdong/ddingdongBE/common/config/SecurityConfig.java @@ -37,7 +37,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthService authSer .antMatchers(GET, API_PREFIX + "/clubs/**", API_PREFIX + "/notices/**", - API_PREFIX + "/banners/**") + API_PREFIX + "/banners/**", + API_PREFIX + "/documents/**", + API_PREFIX + "/questions/**") .permitAll() .antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() .anyRequest() diff --git a/src/main/java/ddingdong/ddingdongBE/common/converter/MultipartJackson2HttpMessageConverter.java b/src/main/java/ddingdong/ddingdongBE/common/converter/MultipartJackson2HttpMessageConverter.java new file mode 100644 index 00000000..18186286 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/converter/MultipartJackson2HttpMessageConverter.java @@ -0,0 +1,31 @@ +package ddingdong.ddingdongBE.common.converter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.lang.reflect.Type; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; + +@Component +public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + return false; + } + + @Override + protected boolean canWrite(MediaType mediaType) { + return false; + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/AuthenticationException.java b/src/main/java/ddingdong/ddingdongBE/common/exception/AuthenticationException.java index f5024c59..9ef22e29 100644 --- a/src/main/java/ddingdong/ddingdongBE/common/exception/AuthenticationException.java +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/AuthenticationException.java @@ -1,15 +1,35 @@ package ddingdong.ddingdongBE.common.exception; -import lombok.Getter; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; -@Getter -public class AuthenticationException extends RuntimeException{ +sealed public class AuthenticationException extends CustomException { - private final ErrorMessage errorMessage; - private final String message; + public static final String UNREGISTERED_ID_ERROR_MESSAGE = "λ“±λ‘λ˜μ§€ μ•Šμ€ IDμž…λ‹ˆλ‹€."; + public static final String INVALIDATED_PASSWORD_ERROR_MESSAGE = "잘λͺ»λœ λΉ„λ°€λ²ˆν˜Έμž…λ‹ˆλ‹€."; + public static final String NON_EXIST_USER_ROLE_ERROR_MESSAGE = "μœ μ € κΆŒν•œμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."; - public AuthenticationException(ErrorMessage errorMessage) { - this.errorMessage = errorMessage; - this.message = errorMessage.getText(); + public AuthenticationException(String message, int errorCode) { + super(message, errorCode); + } + + public static final class UnRegisteredId extends AuthenticationException { + + public UnRegisteredId() { + super(UNREGISTERED_ID_ERROR_MESSAGE, UNAUTHORIZED.value()); + } + } + + public static final class InvalidPassword extends AuthenticationException { + + public InvalidPassword() { + super(INVALIDATED_PASSWORD_ERROR_MESSAGE, UNAUTHORIZED.value()); + } + } + + public static final class NonExistUserRole extends AuthenticationException { + + public NonExistUserRole() { + super(NON_EXIST_USER_ROLE_ERROR_MESSAGE, UNAUTHORIZED.value()); + } } } diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/AwsException.java b/src/main/java/ddingdong/ddingdongBE/common/exception/AwsException.java new file mode 100644 index 00000000..d24accb1 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/AwsException.java @@ -0,0 +1,27 @@ +package ddingdong.ddingdongBE.common.exception; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +sealed public class AwsException extends CustomException { + + public static final String AWS_SERVICE_ERROR_MESSAGE = "AWS μ„œλΉ„μŠ€ 였λ₯˜λ‘œ 인해 Presigned URL 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."; + public static final String AWS_CLIENT_ERROR_MESSAGE = "AWS ν΄λΌμ΄μ–ΈνŠΈ 였λ₯˜λ‘œ 인해 Presigned URL 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."; + + public AwsException(String message, int errorCode) { + super(message, errorCode); + } + + public static final class AwsService extends AwsException { + + public AwsService() { + super(AWS_SERVICE_ERROR_MESSAGE, INTERNAL_SERVER_ERROR.value()); + } + } + + public static final class AwsClient extends AwsException { + + public AwsClient() { + super(AWS_CLIENT_ERROR_MESSAGE, INTERNAL_SERVER_ERROR.value()); + } + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/CustomException.java b/src/main/java/ddingdong/ddingdongBE/common/exception/CustomException.java new file mode 100644 index 00000000..a2188f02 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/CustomException.java @@ -0,0 +1,16 @@ +package ddingdong.ddingdongBE.common.exception; + +import lombok.Getter; + +@Getter +abstract class CustomException extends RuntimeException { + private final String message; + private final int errorCode; + + public CustomException(String message, int errorCode) { + super(message); + this.message = message; + this.errorCode = errorCode; + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/CustomExceptionHandler.java b/src/main/java/ddingdong/ddingdongBE/common/exception/CustomExceptionHandler.java new file mode 100644 index 00000000..1627b8db --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/CustomExceptionHandler.java @@ -0,0 +1,128 @@ +package ddingdong.ddingdongBE.common.exception; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import io.swagger.v3.oas.annotations.Hidden; +import java.time.LocalDateTime; +import java.util.NoSuchElementException; +import javax.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.support.MissingServletRequestPartException; + +@Hidden +@RestControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) +@Slf4j +public class CustomExceptionHandler { + + @ResponseStatus(INTERNAL_SERVER_ERROR) + @ExceptionHandler(Throwable.class) + public ErrorResponse handleSystemException(Throwable exception, HttpServletRequest request) { + String connectionInfo = createLogConnectionInfo(request); + + loggingApplicationError(connectionInfo + + "\n" + + "[SYSTEM-ERROR]" + " : " + exception.getMessage()); + + return new ErrorResponse(INTERNAL_SERVER_ERROR.value(), "Internal Sever Error", LocalDateTime.now()); + } + + @ResponseStatus(BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ErrorResponse handleIllegalArgumentException(IllegalArgumentException exception, + HttpServletRequest request) { + String connectionInfo = createLogConnectionInfo(request); + + loggingApplicationError(connectionInfo + + "\n" + + exception.getClass().getSimpleName() + " : " + exception.getMessage()); + + return new ErrorResponse(BAD_REQUEST.value(), exception.getMessage(), LocalDateTime.now() + ); + } + + @ResponseStatus(BAD_REQUEST) + @ExceptionHandler(CustomException.class) + public ErrorResponse handlePersistenceException(CustomException exception, HttpServletRequest request) { + String connectionInfo = createLogConnectionInfo(request); + + loggingApplicationError(connectionInfo + + "\n" + + exception.getErrorCode() + " : " + exception.getMessage()); + + return new ErrorResponse(exception.getErrorCode(), exception.getMessage(), LocalDateTime.now() + ); + } + + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(AuthenticationException.class) + public ErrorResponse handleAuthenticationException(AuthenticationException exception, HttpServletRequest request) { + String connectionInfo = createLogConnectionInfo(request); + + loggingApplicationError(connectionInfo + + "\n" + + exception.getClass().getSimpleName() + " : " + exception.getMessage()); + + return new ErrorResponse(exception.getErrorCode(), exception.getMessage(), LocalDateTime.now() + ); + } + + @ResponseStatus(BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException exception, + HttpServletRequest request) { + String connectionInfo = createLogConnectionInfo(request); + + String message = exception.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(FieldError::getDefaultMessage) + .orElse("μž…λ ₯된 값이 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + + loggingApplicationError(connectionInfo + + "\n" + + exception.getClass().getSimpleName() + " : " + message); + + return new ErrorResponse(BAD_REQUEST.value(), exception.getMessage(), LocalDateTime.now() + ); + } + + + // TODO : NoSuchElementException λŒ€μ‹  PersistenceException.ResourceNotFound()둜 μ „ν™˜ ν•„μš” + @ExceptionHandler(NoSuchElementException.class) + @ResponseStatus(BAD_REQUEST) + public ExceptionResponse handleNoSuchElementException(NoSuchElementException e) { + return ExceptionResponse.of(BAD_REQUEST, e.getMessage()); + } + + // TODO : presigned url λ„μž… μ‹œ, μ‚­μ œ μ˜ˆμ • + @ExceptionHandler(MissingServletRequestPartException.class) + @ResponseStatus(NOT_FOUND) + public ExceptionResponse handleMissingServletRequestPartException(MissingServletRequestPartException e) { + return ExceptionResponse.of(BAD_REQUEST, e.getMessage()); + } + + + private String createLogConnectionInfo(HttpServletRequest request) { + String requestMethod = request.getMethod(); + String requestUrl = request.getRequestURI(); + String queryString = request.getQueryString(); + String clientIp = request.getHeader("X-Forwarded-For") != null ? request.getHeader("X-Forwarded-For") + : request.getRemoteAddr(); + + return requestMethod + requestUrl + "?" + queryString + " from ip: " + clientIp; + } + + private void loggingApplicationError(String applicationLog) { + log.warn("errorLog = {}", applicationLog); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/ErrorMessage.java b/src/main/java/ddingdong/ddingdongBE/common/exception/ErrorMessage.java index 8d4a269e..8cade054 100644 --- a/src/main/java/ddingdong/ddingdongBE/common/exception/ErrorMessage.java +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/ErrorMessage.java @@ -10,20 +10,21 @@ public enum ErrorMessage { ILLEGAL_CLUB_LOCATION_PATTERN("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 동아리 μœ„μΉ˜ μ–‘μ‹μž…λ‹ˆλ‹€."), ILLEGAL_CLUB_PHONE_NUMBER_PATTERN("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 동아리 μ „ν™”λ²ˆν˜Έ μ–‘μ‹μž…λ‹ˆλ‹€."), ILLEGAL_PASSWORD_PATTERN("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ λΉ„λ°€λ²ˆν˜Έ μ–‘μ‹μž…λ‹ˆλ‹€."), - ILLEGAL_SCORE_CATEGORY("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ μΉ΄ν…Œκ³ λ¦¬ μ–‘μ‹μž…λ‹ˆλ‹€."), - ALREADY_EXIST_CLUB_ID("이미 μ‘΄μž¬ν•˜λŠ” 동아리 κ³„μ •μž…λ‹ˆλ‹€."), + ILLEGAL_SCORE_CATEGORY("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ μ μˆ˜λ³€λ™λ‚΄μ—­ μΉ΄ν…Œκ³ λ¦¬μž…λ‹ˆλ‹€."), NO_SUCH_CLUB("ν•΄λ‹Ή 동아리가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), NO_SUCH_NOTICE("ν•΄λ‹Ή 곡지사항이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), NO_SUCH_ACTIVITY_REPORT("ν•΄λ‹Ή ν™œλ™λ³΄κ³ μ„œκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), NO_SUCH_QR_STAMP_HISTORY("이벀트 μ°Έμ—¬ 내역이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), INVALID_CLUB_SCORE_VALUE("동아리 μ μˆ˜λŠ” 0 ~ 999점 μž…λ‹ˆλ‹€."), - INVALID_PASSWORD("잘λͺ»λœ λΉ„λ°€λ²ˆν˜Έμž…λ‹ˆλ‹€."), INVALID_STAMP_COUNT_FOR_APPLY("μŠ€νƒ¬ν”„λ₯Ό λͺ¨λ‘ λͺ¨μ•„μ•Ό μ΄λ²€νŠΈμ— μ°Έμ—¬ν•  수 μžˆμ–΄μš”!"), ACCESS_DENIED("μ ‘κ·ΌκΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€."), UNREGISTER_ID("λ“±λ‘λ˜μ§€ μ•Šμ€ IDμž…λ‹ˆλ‹€."), - NO_SUCH_BANNER("ν•΄λ‹Ή λ°°λ„ˆκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), NON_VALIDATED_TOKEN("μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."), - NO_SUCH_FIX("ν•΄λ‹Ή 수리 μ‹ μ²­μ„œκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + NO_SUCH_BANNER("ν•΄λ‹Ή λ°°λ„ˆκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), + NO_SUCH_FIX("ν•΄λ‹Ή 수리 μ‹ μ²­μ„œκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), + NO_SUCH_FIX_ZONE_COMMENT("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν”½μŠ€μ‘΄ λŒ“κΈ€μž…λ‹ˆλ‹€."), + NO_SUCH_DOCUMENT("ν•΄λ‹Ή μžλ£Œκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), + NO_SUCH_QUESTION("ν•΄λ‹Ή 질문이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); private final String text; } diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/ErrorResponse.java b/src/main/java/ddingdong/ddingdongBE/common/exception/ErrorResponse.java new file mode 100644 index 00000000..75263dbf --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/ErrorResponse.java @@ -0,0 +1,21 @@ +package ddingdong.ddingdongBE.common.exception; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.Builder; + +@Schema( + name = "ErrorResponse", + description = "μ—λŸ¬ 응닡" +) +@Builder +public record ErrorResponse( + @Schema(description = "μƒνƒœ μ½”λ“œ", example = "400") + int status, + @Schema(description = "μ—λŸ¬ λ©”μ‹œμ§€", example = "μ—λŸ¬ λ©”μ‹œμ§€") + String message, + @Schema(description = "μ—λŸ¬ μ‹œκ°", example = "2024-08-22T00:08:46.990585") + LocalDateTime timestamp +) { + +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/ExceptionController.java b/src/main/java/ddingdong/ddingdongBE/common/exception/ExceptionController.java deleted file mode 100644 index 5a8fb528..00000000 --- a/src/main/java/ddingdong/ddingdongBE/common/exception/ExceptionController.java +++ /dev/null @@ -1,60 +0,0 @@ -package ddingdong.ddingdongBE.common.exception; - -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; - -import java.util.NoSuchElementException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.multipart.support.MissingServletRequestPartException; - -@RestControllerAdvice -@Slf4j -public class ExceptionController { - - @ExceptionHandler(RuntimeException.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public ExceptionResponse handleRuntimeException(RuntimeException e) { - log.info(e.getMessage()); - return ExceptionResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR.getText()); - } - - @ExceptionHandler(Exception.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public ExceptionResponse handleException(Exception e) { - log.info(e.getMessage()); - return ExceptionResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR.getText()); - } - - @ExceptionHandler(IllegalArgumentException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ExceptionResponse handleIllegalArgumentException(IllegalArgumentException e) { - return ExceptionResponse.of(HttpStatus.BAD_REQUEST, e.getMessage()); - } - - @ExceptionHandler(NoSuchElementException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ExceptionResponse handleNoSuchElementException(NoSuchElementException e) { - return ExceptionResponse.of(HttpStatus.BAD_REQUEST, e.getMessage()); - } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException(AuthenticationException e) { - return switch (e.getErrorMessage()) { - case INVALID_PASSWORD, UNREGISTER_ID -> ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(ExceptionResponse.of(HttpStatus.UNAUTHORIZED, e.getMessage())); - - default -> ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ExceptionResponse.of(HttpStatus.BAD_REQUEST, e.getMessage())); - }; - } - - @ExceptionHandler(MissingServletRequestPartException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - public ExceptionResponse handleMissingServletRequestPartException(MissingServletRequestPartException e) { - return ExceptionResponse.of(HttpStatus.BAD_REQUEST, e.getMessage()); - } -} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/ExceptionResponse.java b/src/main/java/ddingdong/ddingdongBE/common/exception/ExceptionResponse.java index fa79e5e2..e40fdb71 100644 --- a/src/main/java/ddingdong/ddingdongBE/common/exception/ExceptionResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/ExceptionResponse.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +// TODO : λͺ¨λ“  μ—λŸ¬ μ „ν™˜ 후에 μ‚­μ œ ν•„μš” @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class ExceptionResponse { diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/InvalidatedMappingException.java b/src/main/java/ddingdong/ddingdongBE/common/exception/InvalidatedMappingException.java new file mode 100644 index 00000000..011e2399 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/InvalidatedMappingException.java @@ -0,0 +1,17 @@ +package ddingdong.ddingdongBE.common.exception; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +sealed public class InvalidatedMappingException extends CustomException { + + public InvalidatedMappingException(String message, int errorCode) { + super(message, errorCode); + } + + public static final class InvalidatedEnumValue extends InvalidatedMappingException { + + public InvalidatedEnumValue(String message) { + super(message, BAD_REQUEST.value()); + } + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/ParsingExcelFileException.java b/src/main/java/ddingdong/ddingdongBE/common/exception/ParsingExcelFileException.java new file mode 100644 index 00000000..3ce9cd0d --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/ParsingExcelFileException.java @@ -0,0 +1,27 @@ +package ddingdong.ddingdongBE.common.exception; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +sealed public class ParsingExcelFileException extends CustomException { + + public static final String NON_EXCEL_FILE_ERROR_MESSAGE = "μ—‘μ…€ 파일(.xls, .xlxs) 이 μ•„λ‹™λ‹ˆλ‹€."; + public static final String EXCEL_IO_ERROR_MESSAGE = "μ˜¬λ°”λ₯Έ μ—‘μ…€ νŒŒμΌμ„ μ‚¬μš©ν•΄μ£Όμ„Έμš”."; + + public ParsingExcelFileException(String message, int errorCode) { + super(message, errorCode); + } + + public static final class NonExcelFile extends ParsingExcelFileException { + + public NonExcelFile() { + super(NON_EXCEL_FILE_ERROR_MESSAGE, BAD_REQUEST.value()); + } + } + + public static final class ExcelIO extends ParsingExcelFileException { + + public ExcelIO() { + super(EXCEL_IO_ERROR_MESSAGE, BAD_REQUEST.value()); + } + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/PersistenceException.java b/src/main/java/ddingdong/ddingdongBE/common/exception/PersistenceException.java new file mode 100644 index 00000000..444dce68 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/PersistenceException.java @@ -0,0 +1,18 @@ +package ddingdong.ddingdongBE.common.exception; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +sealed public class PersistenceException extends CustomException { + + public PersistenceException(String message, int errorCode) { + super(message, errorCode); + } + + public static final class ResourceNotFound extends PersistenceException { + + public ResourceNotFound(String message) { + super(message, NOT_FOUND.value()); + } + + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/exception/RegisterClubException.java b/src/main/java/ddingdong/ddingdongBE/common/exception/RegisterClubException.java new file mode 100644 index 00000000..67d0b6ee --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/exception/RegisterClubException.java @@ -0,0 +1,20 @@ +package ddingdong.ddingdongBE.common.exception; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +sealed public class RegisterClubException extends CustomException { + + public static final String ALREADY_EXIST_CLUB_ID_ERROR_MESSAGE = "이미 μ‘΄μž¬ν•˜λŠ” 동아리 κ³„μ •μž…λ‹ˆλ‹€."; + + public RegisterClubException(String message, int errorCode) { + super(message, errorCode); + } + + + public static final class AlreadyExistClubId extends RegisterClubException { + + public AlreadyExistClubId() { + super(ALREADY_EXIST_CLUB_ID_ERROR_MESSAGE, BAD_REQUEST.value()); + } + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/handler/RestAuthenticationEntryPoint.java b/src/main/java/ddingdong/ddingdongBE/common/handler/RestAuthenticationEntryPoint.java index 3e58a6d5..38c8151f 100644 --- a/src/main/java/ddingdong/ddingdongBE/common/handler/RestAuthenticationEntryPoint.java +++ b/src/main/java/ddingdong/ddingdongBE/common/handler/RestAuthenticationEntryPoint.java @@ -1,14 +1,16 @@ package ddingdong.ddingdongBE.common.handler; -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import ddingdong.ddingdongBE.common.exception.ErrorMessage; -import ddingdong.ddingdongBE.common.exception.ExceptionResponse; +import ddingdong.ddingdongBE.common.exception.ErrorResponse; import java.io.IOException; +import java.time.LocalDateTime; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -18,22 +20,13 @@ public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - - ErrorMessage errorMessage = valueOf(request.getAttribute("exception").toString()); - - if (errorMessage.equals(NON_VALIDATED_TOKEN)) { - responseAuthenticationException(response, errorMessage); - return; - } - responseAuthenticationException(response, INVALID_PASSWORD); - } - - private void responseAuthenticationException(HttpServletResponse response, ErrorMessage errorMessage) - throws IOException { - ExceptionResponse exceptionResponse = ExceptionResponse.of(HttpStatus.UNAUTHORIZED, - errorMessage.getText()); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + ErrorResponse errorResponse = new ErrorResponse(UNAUTHORIZED.value(), + ErrorMessage.NON_VALIDATED_TOKEN.getText(), LocalDateTime.now()); response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write(new ObjectMapper().writeValueAsString(exceptionResponse)); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/api/AdminActivityReportApi.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/api/AdminActivityReportApi.java new file mode 100644 index 00000000..a818d78f --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/api/AdminActivityReportApi.java @@ -0,0 +1,39 @@ +package ddingdong.ddingdongBE.domain.activityreport.api; + +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.CreateActivityTermInfoRequest; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportListResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportTermInfoResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "Activity Report - Admin", description = "Activity Report Admin API") +@RequestMapping("/server/admin/activity-reports") +public interface AdminActivityReportApi { + + @Operation(summary = "ν™œλ™ λ³΄κ³ μ„œ 전체 쑰회") + @GetMapping + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getActivityReports(); + + @Operation(summary = "ν™œλ™ λ³΄κ³ μ„œ νšŒμ°¨λ³„ κΈ°κ°„ 쑰회 API") + @GetMapping("/term") + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getActivityTermInfos(); + + @Operation(summary = "ν™œλ™ λ³΄κ³ μ„œ νšŒμ°¨λ³„ κΈ°κ°„ μ„€μ • API") + @PostMapping("/term") + @ResponseStatus(HttpStatus.CREATED) + @SecurityRequirement(name = "AccessToken") + void createActivityTermInfo(@RequestBody CreateActivityTermInfoRequest request); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/api/ClubActivityReportApi.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/api/ClubActivityReportApi.java new file mode 100644 index 00000000..29a02fde --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/api/ClubActivityReportApi.java @@ -0,0 +1,86 @@ +package ddingdong.ddingdongBE.domain.activityreport.api; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.CreateActivityReportRequest; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.UpdateActivityReportRequest; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportListResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.CurrentTermResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Activity Report - Club", description = "Activity Report Club API") +@RequestMapping("/server/club") +public interface ClubActivityReportApi { + + @Operation(summary = "ν˜„μž¬ ν™œλ™λ³΄κ³ μ„œ 회차 쑰회") + @GetMapping("/activity-reports/current-term") + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + CurrentTermResponse getCurrentTerm(); + + @Operation(summary = "본인 동아리 ν™œλ™λ³΄κ³ μ„œ 전체 쑰회") + @GetMapping("/my/activity-reports") + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getMyActivityReports( + @AuthenticationPrincipal PrincipalDetails principalDetails + ); + + @Operation(summary = "ν™œλ™λ³΄κ³ μ„œ 상세 쑰회") + @GetMapping("/activity-reports") + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getActivityReport( + @RequestParam("term") String term, + @RequestParam("club_name") String clubName + ); + + @Operation(summary = "ν™œλ™λ³΄κ³ μ„œ 등둝") + @PostMapping(value = "/my/activity-reports", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void createActivityReport( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @ModelAttribute(value = "reportData") List requests, + @RequestPart(value = "uploadFiles1", required = false) MultipartFile firstImage, + @RequestPart(value = "uploadFiles2", required = false) MultipartFile secondImage + ); + + @Operation(summary = "ν™œλ™λ³΄κ³ μ„œ μˆ˜μ •") + @PatchMapping(value = "/my/activity-reports", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void updateActivityReport( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestParam(value = "term") String term, + @ModelAttribute(value = "reportData") List requests, + @RequestPart(value = "uploadFiles1", required = false) MultipartFile firstImage, + @RequestPart(value = "uploadFiles2", required = false) MultipartFile secondImage + ); + + @Operation(summary = "ν™œλ™λ³΄κ³ μ„œ μ‚­μ œ") + @DeleteMapping("/my/activity-reports") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void deleteActivityReport( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestParam(value = "term") String term + ); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/AdminActivityReportApiController.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/AdminActivityReportApiController.java index 91bb3579..a51b61b9 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/AdminActivityReportApiController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/AdminActivityReportApiController.java @@ -1,24 +1,36 @@ package ddingdong.ddingdongBE.domain.activityreport.controller; -import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.AllActivityReportResponse; +import ddingdong.ddingdongBE.domain.activityreport.api.AdminActivityReportApi; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.CreateActivityTermInfoRequest; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportListResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportTermInfoResponse; import ddingdong.ddingdongBE.domain.activityreport.service.ActivityReportService; +import ddingdong.ddingdongBE.domain.activityreport.service.ActivityReportTermInfoService; import java.util.List; - import lombok.RequiredArgsConstructor; - import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor -@RequestMapping("/server/admin/activity-reports") -public class AdminActivityReportApiController { +public class AdminActivityReportApiController implements AdminActivityReportApi { private final ActivityReportService activityReportService; + private final ActivityReportTermInfoService activityReportTermInfoService; @GetMapping - public List getActivityReports() { + public List getActivityReports() { return activityReportService.getAll(); } + + @Override + public List getActivityTermInfos() { + return activityReportTermInfoService.getAll(); + } + + @Override + public void createActivityTermInfo(CreateActivityTermInfoRequest request) { + activityReportTermInfoService.create(request.startDate(), request.totalTermCount()); + } + } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/ClubActivityReportApiController.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/ClubActivityReportApiController.java index c464713e..032585fb 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/ClubActivityReportApiController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/ClubActivityReportApiController.java @@ -4,142 +4,111 @@ import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.IMAGE; import ddingdong.ddingdongBE.auth.PrincipalDetails; -import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.RegisterActivityReportRequest; +import ddingdong.ddingdongBE.domain.activityreport.api.ClubActivityReportApi; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.CreateActivityReportRequest; import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.UpdateActivityReportRequest; import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportDto; -import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.AllActivityReportResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportListResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportResponse; import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.CurrentTermResponse; -import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.DetailActivityReportResponse; import ddingdong.ddingdongBE.domain.activityreport.service.ActivityReportService; import ddingdong.ddingdongBE.domain.user.entity.User; import ddingdong.ddingdongBE.file.service.FileService; - import java.util.Collections; import java.util.List; import java.util.stream.IntStream; - import lombok.RequiredArgsConstructor; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor -@RequestMapping("/server/club") -public class ClubActivityReportApiController { +public class ClubActivityReportApiController implements ClubActivityReportApi { private final ActivityReportService activityReportService; private final FileService fileService; - @GetMapping("activity-reports/current-term") public CurrentTermResponse getCurrentTerm() { return activityReportService.getCurrentTerm(); } - @GetMapping("/my/activity-reports") - public List getMyActivityReports( - @AuthenticationPrincipal PrincipalDetails principalDetails - ) { + public List getMyActivityReports(PrincipalDetails principalDetails) { User user = principalDetails.getUser(); return activityReportService.getMyActivityReports(user); } - @GetMapping("/activity-reports") - public List getActivityReport( - @RequestParam("term") String term, - @RequestParam("club_name") String clubName + public List getActivityReport( + String term, + String clubName ) { return activityReportService.getActivityReport(term, clubName); } - @PostMapping("/my/activity-reports") - public void registerReport( - @AuthenticationPrincipal PrincipalDetails principalDetails, - @RequestPart(value = "reportData", required = false) List requests, - @RequestPart(value = "uploadFiles1", required = false) MultipartFile firstImage, - @RequestPart(value = "uploadFiles2", required = false) MultipartFile secondImage + public void createActivityReport( + PrincipalDetails principalDetails, + List requests, + MultipartFile firstImage, + MultipartFile secondImage ) { User user = principalDetails.getUser(); + List images = List.of(firstImage, secondImage); + IntStream.range(0, requests.size()) .forEach(index -> { - - RegisterActivityReportRequest request = requests.get(index); - Long registeredActivityReportId = activityReportService.register(user, request); - - if (index == 0 && firstImage != null && !firstImage.isEmpty()) { - fileService.uploadFile(registeredActivityReportId, - Collections.singletonList(firstImage), - IMAGE, ACTIVITY_REPORT); - } - - if (index == 1 && secondImage != null && !secondImage.isEmpty()) { - fileService.uploadFile(registeredActivityReportId, - Collections.singletonList(secondImage), - IMAGE, ACTIVITY_REPORT); + CreateActivityReportRequest request = requests.get(index); + Long registeredActivityReportId = activityReportService.create(user, request); + + if (index < images.size() && images.get(index) != null && !images.get(index).isEmpty()) { + fileService.uploadFile( + registeredActivityReportId, + Collections.singletonList(images.get(index)), + IMAGE, + ACTIVITY_REPORT + ); } }); + } - @PatchMapping("my/activity-reports") - public void updateReport( - @AuthenticationPrincipal PrincipalDetails principalDetails, - @RequestParam("term") String term, - @RequestPart(value = "reportData", required = false) List requests, - @RequestPart(value = "uploadFiles1", required = false) MultipartFile firstImage, - @RequestPart(value = "uploadFiles2", required = false) MultipartFile secondImage + public void updateActivityReport( + PrincipalDetails principalDetails, + String term, + List requests, + MultipartFile firstImage, + MultipartFile secondImage ) { User user = principalDetails.getUser(); - List updateActivityReportDtos = activityReportService.update(user, term, - requests); + List activityReportDtos = activityReportService.update(user, term, requests); + List images = List.of(firstImage, secondImage); - IntStream.range(0, updateActivityReportDtos.size()) + IntStream.range(0, Math.min(activityReportDtos.size(), images.size())) + .filter(index -> images.get(index) != null && !images.get(index).isEmpty()) .forEach(index -> { - if (index == 0) { - fileService.deleteFile(updateActivityReportDtos.get(index).getId(), IMAGE, - ACTIVITY_REPORT); - - if (!firstImage.isEmpty()) { - fileService.uploadFile(updateActivityReportDtos.get(index).getId(), Collections.singletonList(firstImage), - IMAGE, - ACTIVITY_REPORT); - } - } - if (index == 1) { - fileService.deleteFile(updateActivityReportDtos.get(index).getId(), IMAGE, - ACTIVITY_REPORT); - - if (!secondImage.isEmpty()) { - fileService.uploadFile(updateActivityReportDtos.get(index).getId(), Collections.singletonList(secondImage), - IMAGE, - ACTIVITY_REPORT); - } - } + fileService.deleteFile( + activityReportDtos.get(index).getId(), + IMAGE, + ACTIVITY_REPORT + ); + + fileService.uploadFile( + activityReportDtos.get(index).getId(), + Collections.singletonList(images.get(index)), + IMAGE, + ACTIVITY_REPORT + ); } ); } - @DeleteMapping("my/activity-reports") - public void deleteReport( - @AuthenticationPrincipal PrincipalDetails principalDetails, - @RequestParam("term") String term + public void deleteActivityReport( + PrincipalDetails principalDetails, + String term ) { User user = principalDetails.getUser(); - List deleteActivityReportDtos = activityReportService.delete(user, term); - deleteActivityReportDtos - .forEach( - activityReportDto -> fileService.deleteFile(activityReportDto.getId(), IMAGE, - ACTIVITY_REPORT) - ); + activityReportService.delete(user, term) + .forEach(it -> fileService.deleteFile(it.getId(), IMAGE, ACTIVITY_REPORT)); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/RegisterActivityReportRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/CreateActivityReportRequest.java similarity index 89% rename from src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/RegisterActivityReportRequest.java rename to src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/CreateActivityReportRequest.java index b377b56b..3fe85802 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/RegisterActivityReportRequest.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/CreateActivityReportRequest.java @@ -1,18 +1,17 @@ package ddingdong.ddingdongBE.domain.activityreport.controller.dto.request; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; import ddingdong.ddingdongBE.domain.activityreport.domain.ActivityReport; import ddingdong.ddingdongBE.domain.activityreport.domain.Participant; import ddingdong.ddingdongBE.domain.club.entity.Club; - import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; - import lombok.Getter; @Getter -public class RegisterActivityReportRequest { +public class CreateActivityReportRequest { public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm"; @@ -20,9 +19,11 @@ public class RegisterActivityReportRequest { private String content; private String place; + @JsonInclude(JsonInclude.Include.NON_NULL) @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_FORMAT, timezone = "Asia/Seoul") private LocalDateTime startDate; + @JsonInclude(JsonInclude.Include.NON_NULL) @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_FORMAT, timezone = "Asia/Seoul") private LocalDateTime endDate; @@ -44,4 +45,4 @@ private LocalDateTime parseToDate(final LocalDateTime date) { String dateString = date.format(DateTimeFormatter.ofPattern(DATE_FORMAT)); return LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(DATE_FORMAT)); } -} \ No newline at end of file +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/CreateActivityTermInfoRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/CreateActivityTermInfoRequest.java new file mode 100644 index 00000000..f4c6dc53 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/CreateActivityTermInfoRequest.java @@ -0,0 +1,20 @@ +package ddingdong.ddingdongBE.domain.activityreport.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import javax.validation.constraints.Pattern; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema( + name = "CreateActivityTermInfoRequest", + description = "ν™œλ™ λ³΄κ³ μ„œ 회차 μ‹œμž‘ 기쀀일 μ„€μ • μš”μ²­" +) +public record CreateActivityTermInfoRequest( + @Schema(description = "ν™œλ™ λ³΄κ³ μ„œ μ‹œμž‘ 일자", example = "2024-07-22") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "λ‚ μ§œλŠ” yyyy-MM-dd ν˜•μ‹μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€.") + LocalDate startDate, + @Schema(description = "μ„€μ •ν•  총 회차 수", example = "10 (=총 10회 μ„€μ •)") + int totalTermCount +) { +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/UpdateActivityReportRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/UpdateActivityReportRequest.java index 66dc1354..4dd56618 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/UpdateActivityReportRequest.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/request/UpdateActivityReportRequest.java @@ -1,10 +1,9 @@ package ddingdong.ddingdongBE.domain.activityreport.controller.dto.request; -import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.activityreport.domain.Participant; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.List; - -import ddingdong.ddingdongBE.domain.activityreport.domain.Participant; import lombok.Getter; @Getter @@ -12,12 +11,21 @@ public class UpdateActivityReportRequest { public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm"; + @Schema(description = "회차 정보") private String term; + + @Schema(description = "λ‚΄μš©") private String content; + + @Schema(description = "ν™œλ™ μž₯μ†Œ") private String place; - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_FORMAT, timezone = "Asia/Seoul") + + @Schema(description = "ν™œλ™ μ‹œμž‘ 일자") private LocalDateTime startDate; - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_FORMAT, timezone = "Asia/Seoul") + + @Schema(description = "ν™œλ™ μ’…λ£Œ 일자") private LocalDateTime endDate; + + @Schema(description = "ν™œλ™ μ°Έμ—¬μž λͺ©λ‘") private List participants; } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/AllActivityReportResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportListResponse.java similarity index 52% rename from src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/AllActivityReportResponse.java rename to src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportListResponse.java index 0443427b..d31d0719 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/AllActivityReportResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportListResponse.java @@ -5,7 +5,7 @@ import lombok.Getter; @Getter -public class AllActivityReportResponse { +public class ActivityReportListResponse { private String name; private String term; @@ -13,13 +13,13 @@ public class AllActivityReportResponse { private List activityReports; @Builder - public AllActivityReportResponse(String name, String term, List activityReportDtos) { + public ActivityReportListResponse(String name, String term, List activityReportDtos) { this.name = name; this.term = term; this.activityReports = activityReportDtos; } - public static AllActivityReportResponse of(String name, String term, List activityReportDtos) { - return new AllActivityReportResponse(name, term, activityReportDtos); + public static ActivityReportListResponse of(String name, String term, List activityReportDtos) { + return new ActivityReportListResponse(name, term, activityReportDtos); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/DetailActivityReportResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportResponse.java similarity index 75% rename from src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/DetailActivityReportResponse.java rename to src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportResponse.java index 0da3eee5..d616cd9c 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/DetailActivityReportResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportResponse.java @@ -1,16 +1,14 @@ package ddingdong.ddingdongBE.domain.activityreport.controller.dto.response; -import java.time.LocalDateTime; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonFormat; - import ddingdong.ddingdongBE.domain.activityreport.domain.ActivityReport; import ddingdong.ddingdongBE.domain.activityreport.domain.Participant; +import java.time.LocalDateTime; +import java.util.List; import lombok.Getter; @Getter -public class DetailActivityReportResponse { +public class ActivityReportResponse { private Long id; @JsonFormat(pattern = "yyyy-MM-dd HH:mm") @@ -25,8 +23,9 @@ public class DetailActivityReportResponse { private List imageUrls; private List participants; - public DetailActivityReportResponse(Long id, String name, String content, String place, LocalDateTime startDate, - LocalDateTime endDate, List imageUrls, List participants, LocalDateTime createdAt) { + public ActivityReportResponse(Long id, String name, String content, String place, LocalDateTime startDate, + LocalDateTime endDate, List imageUrls, List participants, + LocalDateTime createdAt) { this.id = id; this.name = name; this.content = content; @@ -38,8 +37,8 @@ public DetailActivityReportResponse(Long id, String name, String content, String this.createdAt = createdAt; } - public static DetailActivityReportResponse from(ActivityReport activityReport, List imageUrls) { - return new DetailActivityReportResponse( + public static ActivityReportResponse of(ActivityReport activityReport, List imageUrls) { + return new ActivityReportResponse( activityReport.getId(), activityReport.getClub().getName(), activityReport.getContent(), diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportTermInfoResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportTermInfoResponse.java new file mode 100644 index 00000000..fb87dc08 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/ActivityReportTermInfoResponse.java @@ -0,0 +1,18 @@ +package ddingdong.ddingdongBE.domain.activityreport.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; + +@Schema( + name = "ActivityReportTermInfoResponse", + description = "ν™œλ™ λ³΄κ³ μ„œ 회차 전체 쑰회 응닡" +) +public record ActivityReportTermInfoResponse( + @Schema(description = "회차") + int term, + @Schema(description = "μ‹œμž‘ 일자", example = "2024-07-22") + LocalDate startDate, + @Schema(description = "마감 일자", example = "2024-08-04") + LocalDate endDate +) { +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/CurrentTermResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/CurrentTermResponse.java index e60f973e..13203aef 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/CurrentTermResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/controller/dto/response/CurrentTermResponse.java @@ -11,7 +11,7 @@ public CurrentTermResponse(String term) { this.term = term; } - public static CurrentTermResponse of(String term) { + public static CurrentTermResponse from(String term) { return new CurrentTermResponse(term); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReport.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReport.java index 1c963002..c940e704 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReport.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReport.java @@ -20,10 +20,16 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update activity_report set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "activity_report") public class ActivityReport extends BaseEntity { @Id @@ -44,6 +50,9 @@ public class ActivityReport extends BaseEntity { @ElementCollection private List participants; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "club_id") private Club club; diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReportTermInfo.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReportTermInfo.java new file mode 100644 index 00000000..3eb92627 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReportTermInfo.java @@ -0,0 +1,50 @@ +package ddingdong.ddingdongBE.domain.activityreport.domain; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; + + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update activity_report_term_info set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "activity_report_term_info") +public class ActivityReportTermInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private int term; + + @Column(nullable = false, columnDefinition = "DATE") + private LocalDate startDate; + + @Column(nullable = false, columnDefinition = "DATE") + private LocalDate endDate; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Builder + public ActivityReportTermInfo(int term, LocalDate startDate, LocalDate endDate) { + this.term = term; + this.startDate = startDate; + this.endDate = endDate; + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/repository/ActivityReportTermInfoRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/repository/ActivityReportTermInfoRepository.java new file mode 100644 index 00000000..61999928 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/repository/ActivityReportTermInfoRepository.java @@ -0,0 +1,7 @@ +package ddingdong.ddingdongBE.domain.activityreport.repository; + +import ddingdong.ddingdongBE.domain.activityreport.domain.ActivityReportTermInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ActivityReportTermInfoRepository extends JpaRepository { +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/service/ActivityReportService.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/service/ActivityReportService.java index 4bbd2f19..dd4e3782 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/service/ActivityReportService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/service/ActivityReportService.java @@ -1,62 +1,52 @@ package ddingdong.ddingdongBE.domain.activityreport.service; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.ACTIVITY_REPORT; -import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.CLUB_INTRODUCE; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.IMAGE; -import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.RegisterActivityReportRequest; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.CreateActivityReportRequest; import ddingdong.ddingdongBE.domain.activityreport.controller.dto.request.UpdateActivityReportRequest; import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportDto; -import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.AllActivityReportResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportListResponse; +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportResponse; import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.CurrentTermResponse; -import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.DetailActivityReportResponse; import ddingdong.ddingdongBE.domain.activityreport.domain.ActivityReport; import ddingdong.ddingdongBE.domain.activityreport.repository.ActivityReportRepository; - import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.club.service.ClubService; -import ddingdong.ddingdongBE.domain.fileinformation.entity.FileInformation; import ddingdong.ddingdongBE.domain.fileinformation.service.FileInformationService; import ddingdong.ddingdongBE.domain.user.entity.User; - import java.time.Duration; import java.time.LocalDate; - import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; - import lombok.RequiredArgsConstructor; - import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -@Transactional +@Transactional(readOnly = true) @RequiredArgsConstructor public class ActivityReportService { - private static final String START_DATE = "2024-03-04"; + private static final String START_DATE = "2024-09-02"; private static final int DEFAULT_TERM = 1; private static final int CORRECTION_VALUE = 1; private static final int TERM_LENGTH_OF_DAYS = 14; - private final ClubService clubService; private final FileInformationService fileInformationService; private final ActivityReportRepository activityReportRepository; - @Transactional(readOnly = true) - public List getAll() { + public List getAll() { List activityReports = activityReportRepository.findAll(); return parseToActivityReportResponse(activityReports); } - @Transactional(readOnly = true) - public List getMyActivityReports(final User user) { - Club club = clubService.findClubByUserId(user.getId()); + public List getMyActivityReports(final User user) { + Club club = clubService.getByUserId(user.getId()); List activityReports = activityReportRepository.findByClubName( club.getName()); @@ -64,46 +54,57 @@ public List getMyActivityReports(final User user) { return parseToActivityReportResponse(activityReports); } - @Transactional(readOnly = true) - public List getActivityReport(final String term, - final String clubName) { - List activityReports = activityReportRepository.findByClubNameAndTerm( - clubName, term); + public List getActivityReport( + final String term, + final String clubName + ) { + List activityReports = activityReportRepository.findByClubNameAndTerm(clubName, term); return activityReports.stream().map(activityReport -> { List imageUrls = fileInformationService.getImageUrls( IMAGE.getFileType() + ACTIVITY_REPORT.getFileDomain() + activityReport.getId()); - return DetailActivityReportResponse.from(activityReport, imageUrls); + return ActivityReportResponse.of(activityReport, imageUrls); }).collect(Collectors.toList()); } - public Long register(final User user, - final RegisterActivityReportRequest registerActivityReportRequest) { - - Club club = clubService.findClubByUserId(user.getId()); - ActivityReport activityReport = registerActivityReportRequest.toEntity(club); + @Transactional + public Long create( + final User user, + final CreateActivityReportRequest createActivityReportRequest + ) { + Club club = clubService.getByUserId(user.getId()); + ActivityReport activityReport = createActivityReportRequest.toEntity(club); ActivityReport savedActivityReport = activityReportRepository.save(activityReport); return savedActivityReport.getId(); } - public List update(final User user, final String term, - final List requests) { - Club club = clubService.findClubByUserId(user.getId()); + @Transactional + public List update( + final User user, + final String term, + final List requests + ) { + Club club = clubService.getByUserId(user.getId()); List activityReports = activityReportRepository.findByClubNameAndTerm( - club.getName(), term); + club.getName(), + term + ); return IntStream.range(0, activityReports.size()) - .mapToObj(index -> { - activityReports.get(index).update(requests.get(index)); - return ActivityReportDto.from(activityReports.get(index)); - }).collect(Collectors.toList()); + .mapToObj(index -> + { + activityReports.get(index).update(requests.get(index)); + return ActivityReportDto.from(activityReports.get(index)); + } + ).collect(Collectors.toList()); } + @Transactional public List delete(final User user, final String term) { - Club club = clubService.findClubByUserId(user.getId()); + Club club = clubService.getByUserId(user.getId()); List activityReports = activityReportRepository.findByClubNameAndTerm( club.getName(), term); @@ -119,7 +120,7 @@ public CurrentTermResponse getCurrentTerm() { LocalDate currentDate = LocalDate.now(); int gapOfDays = calculateGapOfDays(startDate, currentDate); - return CurrentTermResponse.of(calculateCurrentTerm(gapOfDays)); + return CurrentTermResponse.from(calculateCurrentTerm(gapOfDays)); } private int calculateGapOfDays(final LocalDate startDate, final LocalDate currentDate) { @@ -137,7 +138,7 @@ private String calculateCurrentTerm(final int days) { return String.valueOf(result); } - private List parseToActivityReportResponse( + private List parseToActivityReportResponse( final List activityReports) { Map>> groupedData = activityReports.stream().collect( Collectors.groupingBy(activityReport -> activityReport.getClub().getName(), @@ -153,7 +154,7 @@ private List parseToActivityReportResponse( List activityReportDtos = termEntry.getValue().stream() .map(ActivityReportDto::new) .collect(Collectors.toList()); - return AllActivityReportResponse.of(clubName, term, activityReportDtos); + return ActivityReportListResponse.of(clubName, term, activityReportDtos); }); }).collect(Collectors.toList()); diff --git a/src/main/java/ddingdong/ddingdongBE/domain/activityreport/service/ActivityReportTermInfoService.java b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/service/ActivityReportTermInfoService.java new file mode 100644 index 00000000..f33938ac --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/activityreport/service/ActivityReportTermInfoService.java @@ -0,0 +1,47 @@ +package ddingdong.ddingdongBE.domain.activityreport.service; + +import ddingdong.ddingdongBE.domain.activityreport.controller.dto.response.ActivityReportTermInfoResponse; +import ddingdong.ddingdongBE.domain.activityreport.domain.ActivityReportTermInfo; +import ddingdong.ddingdongBE.domain.activityreport.repository.ActivityReportTermInfoRepository; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ActivityReportTermInfoService { + + private final ActivityReportTermInfoRepository activityReportTermInfoRepository; + + public List getAll() { + List termInfos = activityReportTermInfoRepository.findAll(); + + return termInfos.stream() + .map(termInfo -> new ActivityReportTermInfoResponse( + termInfo.getTerm(), + termInfo.getStartDate(), + termInfo.getEndDate() + )) + .toList(); + } + + public void create(LocalDate startDate, int totalTermCount) { + activityReportTermInfoRepository.saveAll( + IntStream.range(0, totalTermCount) + .mapToObj(i -> { + LocalDate termStartDate = startDate.plusDays(i * 14L); + LocalDate termEndDate = termStartDate.plusDays(13L); + return ActivityReportTermInfo.builder() + .term(i + 1) + .startDate(termStartDate) + .endDate(termEndDate) + .build(); + }) + .collect(Collectors.toList()) + ); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/banner/entity/Banner.java b/src/main/java/ddingdong/ddingdongBE/domain/banner/entity/Banner.java index 5d96782a..a73c0b0e 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/banner/entity/Banner.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/banner/entity/Banner.java @@ -3,6 +3,8 @@ import ddingdong.ddingdongBE.common.BaseEntity; import ddingdong.ddingdongBE.domain.banner.controller.dto.request.UpdateBannerRequest; import ddingdong.ddingdongBE.domain.user.entity.User; +import java.time.LocalDateTime; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; @@ -14,10 +16,16 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update banner set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "banner") public class Banner extends BaseEntity { @Id @@ -34,6 +42,9 @@ public class Banner extends BaseEntity { private String colorCode; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder public Banner(User user, String title, String subTitle, String colorCode) { this.user = user; diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/api/CentralClubApi.java b/src/main/java/ddingdong/ddingdongBE/domain/club/api/CentralClubApi.java new file mode 100644 index 00000000..42e08525 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/api/CentralClubApi.java @@ -0,0 +1,125 @@ +package ddingdong.ddingdongBE.domain.club.api; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.common.exception.ErrorResponse; +import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubMemberRequest; +import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubRequest; +import ddingdong.ddingdongBE.domain.club.controller.dto.response.DetailClubResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Club - Club", description = "Club CentralClub API") +@RequestMapping("/server/club/my") +public interface CentralClubApi { + + @Operation(summary = "동아리원 λͺ…단 λ‹€μš΄λ‘œλ“œ API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "λͺ…단 λ‹€μš΄λ‘œλ“œ 성곡")}) + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + @GetMapping("/club-members/excel") + ResponseEntity getMyClubMemberListFile(@AuthenticationPrincipal PrincipalDetails principalDetails); + + @Operation(summary = "λ‚΄ 동아리 정보 쑰회 API") + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + @GetMapping + DetailClubResponse getMyClub(@AuthenticationPrincipal PrincipalDetails principalDetails); + + @Operation(summary = "λ‚΄ 동아리 정보 μˆ˜μ • API") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + @PatchMapping + void updateClub(@AuthenticationPrincipal PrincipalDetails principalDetails, + @ModelAttribute UpdateClubRequest param, + @RequestPart(name = "profileImage", required = false) List profileImage, + @RequestPart(name = "introduceImages", required = false) List images); + + @Operation(summary = "동아리원 λͺ…단 등둝 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "λͺ…단 등둝 성곡"), + @ApiResponse(responseCode = "400", + description = "잘λͺ»λœ μš”μ²­", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "μ—‘μ…€ 파일이 μ•„λ‹˜", + value = """ + { + "status": 400, + "message": "μ—‘μ…€ 파일이 μ•„λ‹™λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """), + @ExampleObject(name = "잘λͺ»λœ μ—‘μ…€ 파일", + value = """ + { + "status": 400, + "message": "μ˜¬λ°”λ₯Έ μ—‘μ…€ νŒŒμΌμ„ μ‚¬μš©ν•΄μ£Όμ„Έμš”.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """), + @ExampleObject(name = "잘λͺ»λœ 동아리원 μ—­ν• ", + value = """ + { + "status": 400, + "message": "λ™μ•„λ¦¬μ›μ˜ 역할은 LEADER, EXECUTIVE, MEMBER 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """) + }) + ) + }) + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + @PostMapping(value = "/club-members", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + void updateClubMemberList(@AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestPart(name = "file") MultipartFile clubMemberListFile); + + @Operation(summary = "동아리원 정보 μˆ˜μ • API") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "μˆ˜μ • 성곡"), + @ApiResponse(responseCode = "400", + description = "잘λͺ»λœ μš”μ²­", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "잘λͺ»λœ 동아리원 μ—­ν• ", + value = """ + { + "status": 400, + "message": "λ™μ•„λ¦¬μ›μ˜ 역할은 LEADER, EXECUTIVE, MEMBER 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """) + }) + ) + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + @PatchMapping("/club-members/{clubMemberId}") + void updateClubMembers(@PathVariable Long clubMemberId, + @RequestBody @Valid UpdateClubMemberRequest request); +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/AdminClubApiController.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/AdminClubApiController.java index 24fe0045..7eec5c1d 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/AdminClubApiController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/AdminClubApiController.java @@ -22,12 +22,12 @@ public class AdminClubApiController { @PostMapping public void register(@RequestBody RegisterClubRequest registerClubRequest) { - clubService.register(registerClubRequest); + clubService.create(registerClubRequest); } @GetMapping public List getClubs() { - return clubService.getAllForAdmin(); + return clubService.findAllForAdmin(); } @DeleteMapping("/{clubId}") diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/CentralClubApiController.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/CentralClubApiController.java index 4208b598..999cef93 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/CentralClubApiController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/CentralClubApiController.java @@ -1,50 +1,62 @@ package ddingdong.ddingdongBE.domain.club.controller; -import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.*; +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.CLUB_INTRODUCE; +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.CLUB_PROFILE; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.IMAGE; import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.club.api.CentralClubApi; import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubMemberRequest; import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubRequest; import ddingdong.ddingdongBE.domain.club.controller.dto.response.DetailClubResponse; -import ddingdong.ddingdongBE.domain.club.service.ClubMemberService; import ddingdong.ddingdongBE.domain.club.service.ClubService; +import ddingdong.ddingdongBE.domain.club.service.FacadeClubMemberService; import ddingdong.ddingdongBE.domain.user.entity.User; import ddingdong.ddingdongBE.file.service.FileService; - +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; - -import java.util.Optional; import lombok.RequiredArgsConstructor; - import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController -@RequestMapping("/server/club/my") @RequiredArgsConstructor @Slf4j -public class CentralClubApiController { +public class CentralClubApiController implements CentralClubApi { private final ClubService clubService; - private final ClubMemberService clubMemberService; + private final FacadeClubMemberService facadeClubMemberService; private final FileService fileService; - @GetMapping + @Override + public ResponseEntity getMyClubMemberListFile(PrincipalDetails principalDetails) { + User user = principalDetails.getUser(); + byte[] clubMemberListFileData = facadeClubMemberService.getClubMemberListFile(user.getId()); + String filename = "동아리원λͺ…단.xlsx"; + String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename); + + return ResponseEntity.ok() + .headers(headers) + .body(clubMemberListFileData); + } + public DetailClubResponse getMyClub(@AuthenticationPrincipal PrincipalDetails principalDetails) { User user = principalDetails.getUser(); return clubService.getMyClub(user.getId()); } - @PatchMapping() public void updateClub(@AuthenticationPrincipal PrincipalDetails principalDetails, @ModelAttribute UpdateClubRequest param, @RequestPart(name = "profileImage", required = false) List profileImage, @@ -63,12 +75,15 @@ public void updateClub(@AuthenticationPrincipal PrincipalDetails principalDetail } } - @PutMapping(value = "/club-members") - public void updateClubMembers(@AuthenticationPrincipal PrincipalDetails principalDetails, - @RequestPart(value = "data", required = false) UpdateClubMemberRequest request, - @RequestPart(name = "file", required = false) Optional clubMemberListFile) { + @Override + public void updateClubMemberList(PrincipalDetails principalDetails, MultipartFile clubMemberListFile) { User user = principalDetails.getUser(); - clubMemberService.updateClubMembers(user.getId(), request, clubMemberListFile); + facadeClubMemberService.updateMemberList(user.getId(), clubMemberListFile); } + @Override + public void updateClubMembers(Long clubMemberId, + UpdateClubMemberRequest request) { + facadeClubMemberService.update(clubMemberId, request.toCommand()); + } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/UserClubApiController.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/UserClubApiController.java index 2285a326..a8f96606 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/UserClubApiController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/UserClubApiController.java @@ -20,12 +20,12 @@ public class UserClubApiController { @GetMapping public List getClubs() { - return clubService.getAllClubs(LocalDateTime.now()); + return clubService.findAllWithRecruitTimeCheckPoint(LocalDateTime.now()); } @GetMapping("/{clubId}") public DetailClubResponse getDetailClub(@PathVariable Long clubId) { - return clubService.getClub(clubId); + return clubService.findByClubId(clubId); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/ClubMemberDto.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/ClubMemberDto.java deleted file mode 100644 index 14b22900..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/ClubMemberDto.java +++ /dev/null @@ -1,99 +0,0 @@ -package ddingdong.ddingdongBE.domain.club.controller.dto.request; - -import ddingdong.ddingdongBE.domain.club.entity.Club; -import ddingdong.ddingdongBE.domain.club.entity.ClubMember; -import ddingdong.ddingdongBE.domain.club.entity.Position; -import java.util.Arrays; -import java.util.Iterator; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.Row; - -@Getter -@NoArgsConstructor -public class ClubMemberDto { - - private Long id; - - private String name; - - private String studentNumber; - - private String phoneNumber; - - private String position; - - private String department; - - @Builder - public ClubMemberDto(Long id, String name, String studentNumber, String phoneNumber, String position, - String department) { - this.id = id; - this.name = name; - this.studentNumber = studentNumber; - this.phoneNumber = phoneNumber; - this.position = position; - this.department = department; - } - - public static ClubMemberDto from(ClubMember clubMember) { - return ClubMemberDto.builder() - .id(clubMember.getId()) - .name(clubMember.getName()) - .studentNumber(clubMember.getStudentNumber()) - .phoneNumber(clubMember.getPhoneNumber()) - .position(clubMember.getPosition().getName()) - .department(clubMember.getDepartment()).build(); - } - - public ClubMember toEntity(Club club) { - return ClubMember.builder() - .club(club) - .name(name) - .studentNumber(studentNumber) - .phoneNumber(phoneNumber) - .position(Position.valueOf(position)) - .department(department).build(); - } - - public static ClubMemberDto fromExcelRow(Row row) { - ClubMemberDto clubMemberDto = ClubMemberDto.builder().build(); - Iterator cellIterator = row.cellIterator(); - while (cellIterator.hasNext()) { - Cell cell = cellIterator.next(); - if (cell.getCellType() == CellType.STRING) { - if (cell.getStringCellValue() != null) { - clubMemberDto.setValueByCell(cell.getStringCellValue(), cell.getColumnIndex()); - } - } else if (cell.getCellType() == CellType.NUMERIC) { - if (cell.getNumericCellValue() != 0) { - clubMemberDto.setValueByCell(String.valueOf(cell.getNumericCellValue()), cell.getColumnIndex()); - } - } - - } - return clubMemberDto; - } - - private void setValueByCell(String stringCellValue, int columnIndex) { - switch (columnIndex) { - case 0 -> this.name = stringCellValue; - case 1 -> this.studentNumber = stringCellValue; - case 2 -> this.phoneNumber = stringCellValue; - case 3 -> { - validatePositionValue(stringCellValue); - this.position = stringCellValue; - } - case 4 -> this.department = stringCellValue; - } - } - - private void validatePositionValue(String stringCellValue) { - if (Arrays.stream(Position.values()).noneMatch(position-> position.name().equals(stringCellValue))) { - throw new IllegalArgumentException("λ™μ•„λ¦¬μ›μ˜ 역할은 LEADER, EXECUTIVE, MEMBER 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€. "); - } - } -} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/RegisterClubRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/RegisterClubRequest.java index 89c0d7cd..b21ae364 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/RegisterClubRequest.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/RegisterClubRequest.java @@ -29,9 +29,9 @@ public Club toEntity(User user) { .category(category) .tag(tag) .leader(leaderName) - .location(Location.of("S0000")) - .phoneNumber(PhoneNumber.of("010-0000-0000")) - .score(Score.of(0)).build(); + .location(Location.from("S0000")) + .phoneNumber(PhoneNumber.from("010-0000-0000")) + .score(Score.from(0)).build(); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/UpdateClubMemberRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/UpdateClubMemberRequest.java index 05f3b983..50c40d94 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/UpdateClubMemberRequest.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/request/UpdateClubMemberRequest.java @@ -1,13 +1,49 @@ package ddingdong.ddingdongBE.domain.club.controller.dto.request; -import java.util.List; -import lombok.Getter; -import lombok.NoArgsConstructor; +import ddingdong.ddingdongBE.domain.club.entity.Position; +import ddingdong.ddingdongBE.domain.club.service.dto.UpdateClubMemberCommand; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Builder; -@Getter -@NoArgsConstructor -public class UpdateClubMemberRequest { +@Schema( + name = "UpdateClubMemberRequest", + description = "동아리원 정보 μˆ˜μ • μš”μ²­" +) +@Builder +public record UpdateClubMemberRequest( - List clubMemberList; + @Schema(description = "이름", example = "홍길동") + @NotNull(message = "이름은 ν•„μˆ˜λ‘œ μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.") + String name, + + @Schema(description = "ν•™λ²ˆ", example = "60001234") + @NotNull(message = "ν•™λ²ˆμ€ ν•„μˆ˜λ‘œ μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.") + String studentNumber, + + @Schema(description = "μ „ν™”λ²ˆν˜Έ", example = "010-1234-5678") + @NotNull(message = "μ „ν™”λ²ˆν˜ΈλŠ” ν•„μˆ˜λ‘œ μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.") + String phoneNumber, + + @Schema(description = "동아리원 μ—­ν• ", + example = "LEADER", + allowableValues = {"LEADER", "EXECUTION", "MEMBER"} + ) + @NotNull(message = "역할은 ν•„μˆ˜λ‘œ μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.") + String position, + + @Schema(description = "ν•™κ³Ό(λΆ€)", example = "μœ΅ν•©μ†Œν”„νŠΈμ›¨μ–΄ν•™λΆ€") + @NotNull(message = "ν•™κ³Ό(λΆ€)λŠ” ν•„μˆ˜λ‘œ μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.") + String department +) { + + public UpdateClubMemberCommand toCommand() { + return UpdateClubMemberCommand.builder() + .name(name) + .studentNumber(studentNumber) + .phoneNumber(phoneNumber) + .position(Position.from(position)) + .department(department).build(); + } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/response/ClubMemberResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/response/ClubMemberResponse.java new file mode 100644 index 00000000..640df46a --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/response/ClubMemberResponse.java @@ -0,0 +1,46 @@ +package ddingdong.ddingdongBE.domain.club.controller.dto.response; + +import ddingdong.ddingdongBE.domain.club.entity.ClubMember; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ClubMemberResponse { + + private Long id; + + private String name; + + private String studentNumber; + + private String phoneNumber; + + private String position; + + private String department; + + @Builder + public ClubMemberResponse(Long id, String name, String studentNumber, String phoneNumber, + String position, + String department) { + this.id = id; + this.name = name; + this.studentNumber = studentNumber; + this.phoneNumber = phoneNumber; + this.position = position; + this.department = department; + } + + public static ClubMemberResponse from(ClubMember clubMember) { + return ClubMemberResponse.builder() + .id(clubMember.getId()) + .name(clubMember.getName()) + .studentNumber(clubMember.getStudentNumber()) + .phoneNumber(clubMember.getPhoneNumber()) + .position(clubMember.getPosition().getName()) + .department(clubMember.getDepartment()).build(); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/response/DetailClubResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/response/DetailClubResponse.java index 7e23298c..4fa8dab2 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/response/DetailClubResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/controller/dto/response/DetailClubResponse.java @@ -1,7 +1,6 @@ package ddingdong.ddingdongBE.domain.club.controller.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; -import ddingdong.ddingdongBE.domain.club.controller.dto.request.ClubMemberDto; import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.club.entity.Location; import ddingdong.ddingdongBE.domain.club.entity.PhoneNumber; @@ -47,14 +46,14 @@ public class DetailClubResponse { private List introduceImageUrls; - private List clubMembers; + private List clubMembers; @Builder public DetailClubResponse(String name, String category, String tag, String content, String leader, PhoneNumber phoneNumber, Location location, LocalDateTime startRecruitPeriod, LocalDateTime endRecruitPeriod, String regularMeeting, String introduction, - String activity, String ideal, String formUrl, List clubMembers, + String activity, String ideal, String formUrl, List clubMembers, List profileImageUrls, List introduceImageUrls) { this.name = name; this.category = category; @@ -76,7 +75,7 @@ public DetailClubResponse(String name, String category, String tag, String conte } public static DetailClubResponse of(Club club, List profileImageUrls, List introduceImageUrls, - List clubMembers) { + List clubMembers) { return DetailClubResponse.builder() .name(club.getName()) .category(club.getCategory()) diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Club.java b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Club.java index 16a14a86..b77295df 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Club.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Club.java @@ -4,11 +4,11 @@ import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubRequest; import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; import ddingdong.ddingdongBE.domain.user.entity.User; - import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -16,17 +16,22 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; - import javax.persistence.OneToMany; import javax.persistence.OneToOne; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update club set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "club") public class Club extends BaseEntity { @Id @@ -73,9 +78,13 @@ public class Club extends BaseEntity { @Embedded private Score score; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder - public Club(User user, String name, String category, String tag, String leader, Location location, + public Club(Long id, User user, String name, String category, String tag, String leader, Location location, PhoneNumber phoneNumber, Score score) { + this.id = id; this.user = user; this.name = name; this.category = category; @@ -93,8 +102,8 @@ public void updateClubInfo(UpdateClubRequest request) { this.content = request.getContent() != null ? request.getContent() : this.content; this.leader = request.getClubLeader() != null ? request.getClubLeader() : this.leader; this.phoneNumber = - request.getPhoneNumber() != null ? PhoneNumber.of(request.getPhoneNumber()) : this.phoneNumber; - this.location = request.getLocation() != null ? Location.of(request.getLocation()) : this.location; + request.getPhoneNumber() != null ? PhoneNumber.from(request.getPhoneNumber()) : this.phoneNumber; + this.location = request.getLocation() != null ? Location.from(request.getLocation()) : this.location; this.startRecruitPeriod = request.getStartRecruitPeriod().isBlank() ? null : parseLocalDateTime(request.getStartRecruitPeriod()); this.endRecruitPeriod = @@ -106,14 +115,13 @@ public void updateClubInfo(UpdateClubRequest request) { this.formUrl = request.getFormUrl() != null ? request.getFormUrl() : this.formUrl; } - private static LocalDateTime parseLocalDateTime(String inputLocalDateTimeFormat) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - return LocalDateTime.parse(inputLocalDateTimeFormat, formatter); - } - public float editScore(Score score) { this.score = score; - return this.score.getValue(); } + + private LocalDateTime parseLocalDateTime(String inputLocalDateTimeFormat) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return LocalDateTime.parse(inputLocalDateTimeFormat, formatter); + } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/ClubMember.java b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/ClubMember.java index 544b51d2..a90bedb6 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/ClubMember.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/ClubMember.java @@ -1,7 +1,8 @@ package ddingdong.ddingdongBE.domain.club.entity; import ddingdong.ddingdongBE.common.BaseEntity; -import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubMemberRequest; +import java.time.LocalDateTime; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -15,10 +16,16 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update club_member set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "club_member") public class ClubMember extends BaseEntity { @Id @@ -40,6 +47,9 @@ public class ClubMember extends BaseEntity { private String department; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder public ClubMember(Long id, Club club, String name, String studentNumber, String phoneNumber, Position position, String department) { @@ -51,4 +61,12 @@ public ClubMember(Long id, Club club, String name, String studentNumber, String this.position = position; this.department = department; } + + public void updateInformation(ClubMember updateClubMember) { + this.name = updateClubMember.getName(); + this.studentNumber = updateClubMember.getStudentNumber(); + this.phoneNumber = updateClubMember.getPhoneNumber(); + this.position = updateClubMember.getPosition(); + this.department = updateClubMember.getDepartment(); + } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Location.java b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Location.java index c254f5fa..c806dc0d 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Location.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Location.java @@ -6,12 +6,14 @@ import javax.persistence.Column; import javax.persistence.Embeddable; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder public class Location { private static final String LOCATION_REGEX = "^S[0-9]{4,5}"; @@ -20,7 +22,6 @@ public class Location { private String value; private Location(String value) { - validateLocation(value); this.value = value; } @@ -41,12 +42,12 @@ public int hashCode() { return Objects.hash(getValue()); } - public static Location of(String value) { - - return new Location(value); + public static Location from(String value) { + validateLocation(value); + return Location.builder().value(value).build(); } - private void validateLocation(String value) { + private static void validateLocation(String value) { if (!value.matches(LOCATION_REGEX)) { throw new IllegalArgumentException(ILLEGAL_CLUB_LOCATION_PATTERN.getText()); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/PhoneNumber.java b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/PhoneNumber.java index d89388da..2ceeb129 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/PhoneNumber.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/PhoneNumber.java @@ -1,6 +1,6 @@ package ddingdong.ddingdongBE.domain.club.entity; -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; +import static ddingdong.ddingdongBE.common.exception.ErrorMessage.ILLEGAL_CLUB_PHONE_NUMBER_PATTERN; import java.util.Objects; import javax.persistence.Access; @@ -8,6 +8,7 @@ import javax.persistence.Column; import javax.persistence.Embeddable; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -15,6 +16,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Access(AccessType.FIELD) +@Builder public class PhoneNumber { private static final String PHONE_NUMBER_REGEX = "010-\\d{3,4}-\\d{4}"; @@ -23,7 +25,6 @@ public class PhoneNumber { private String number; private PhoneNumber(String number) { - validate(number); this.number = number; } @@ -44,11 +45,14 @@ public int hashCode() { return Objects.hash(getNumber()); } - public static PhoneNumber of(String phoneNumber) { - return new PhoneNumber(phoneNumber); + public static PhoneNumber from(String phoneNumber) { + validate(phoneNumber); + return PhoneNumber.builder() + .number(phoneNumber) + .build(); } - private void validate(String number) { + private static void validate(String number) { if (!number.matches(PHONE_NUMBER_REGEX)) { throw new IllegalArgumentException(ILLEGAL_CLUB_PHONE_NUMBER_PATTERN.getText()); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Position.java b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Position.java index e97d32e2..d8e962bf 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Position.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/entity/Position.java @@ -1,5 +1,6 @@ package ddingdong.ddingdongBE.domain.club.entity; +import ddingdong.ddingdongBE.common.exception.InvalidatedMappingException.InvalidatedEnumValue; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -12,4 +13,12 @@ public enum Position { MEMBER("동아리원"); private final String name; + + public static Position from(String position) { + try { + return Position.valueOf(position); + } catch (IllegalArgumentException e) { + throw new InvalidatedEnumValue("λ™μ•„λ¦¬μ›μ˜ 역할은 LEADER, EXECUTIVE, MEMBER 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€."); + } + } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/repository/ClubMemberRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/club/repository/ClubMemberRepository.java index 38cf9c6e..08b29eda 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/repository/ClubMemberRepository.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/repository/ClubMemberRepository.java @@ -3,17 +3,18 @@ import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.club.entity.ClubMember; + import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; - public interface ClubMemberRepository extends JpaRepository { + List findClubMembersByClubId(Long clubId); @Modifying @Query("delete from ClubMember c where c.club = :club") void deleteAllByClub(@Param("club") Club club); -} \ No newline at end of file +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubMemberService.java b/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubMemberService.java index 9d3f9bc6..f2194f39 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubMemberService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubMemberService.java @@ -1,94 +1,33 @@ package ddingdong.ddingdongBE.domain.club.service; -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.NO_SUCH_CLUB; - -import ddingdong.ddingdongBE.domain.club.controller.dto.request.ClubMemberDto; -import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubMemberRequest; -import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.common.exception.PersistenceException.ResourceNotFound; import ddingdong.ddingdongBE.domain.club.entity.ClubMember; import ddingdong.ddingdongBE.domain.club.repository.ClubMemberRepository; -import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; - -import java.io.IOException; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; - -import javax.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.transaction.annotation.Transactional; @Service -@Transactional +@Transactional(readOnly = true) @RequiredArgsConstructor public class ClubMemberService { - private final ClubRepository clubRepository; private final ClubMemberRepository clubMemberRepository; - public void updateClubMembers(Long userId, UpdateClubMemberRequest request, - Optional clubMemberListFile) { - if (clubMemberListFile.isPresent()) { - MultipartFile file = clubMemberListFile.get(); - isExcelFile(file); - - Club club = clubRepository.findByUserId(userId) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_CLUB.getText())); - List requestedClubMemberDtos = parsingClubMemberListFile(file); - List requestedClubMembers = requestedClubMemberDtos.stream() - .map(clubMemberDto -> clubMemberDto.toEntity(club)) - .toList(); - - clubMemberRepository.deleteAllByClub(club); - clubMemberRepository.saveAll(requestedClubMembers); - return; - } - - Club club = clubRepository.findByUserId(userId) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_CLUB.getText())); - - List memberIds = clubMemberRepository.findClubMembersByClubId(club.getId()) - .stream() - .map(ClubMember::getId) - .toList(); - - List requestedClubMembers = request.getClubMemberList().stream() - .map(clubMemberDto -> clubMemberDto.toEntity(club)) - .toList(); - - if (!memberIds.isEmpty()) { - clubMemberRepository.deleteAllById(memberIds); - } - - clubMemberRepository.saveAll(requestedClubMembers); + public ClubMember getById(Long clubMemberId) { + return clubMemberRepository.findById(clubMemberId) + .orElseThrow(() -> new ResourceNotFound("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ™μ•„λ¦¬μ›μž…λ‹ˆλ‹€.")); } - private void isExcelFile(MultipartFile file) { - String fileName = file.getOriginalFilename(); - if (fileName != null && !(fileName.endsWith(".xls") || fileName.endsWith(".xlsx"))) { - throw new IllegalArgumentException("μ—‘μ…€νŒŒμΌμ΄ μ•„λ‹™λ‹ˆλ‹€."); - } + @Transactional + public void saveAll(List clubMembers) { + clubMemberRepository.saveAll(clubMembers); } - private static List parsingClubMemberListFile(MultipartFile clubMemberListFile) { - List requestedClubMemberDtos = new ArrayList<>(); - try { - Workbook workbook = new XSSFWorkbook(clubMemberListFile.getInputStream()); - Sheet sheet = workbook.getSheetAt(0); - for (Row row : sheet) { - if (row.getRowNum() != 0 && row.getCell(row.getFirstCellNum()).getCellType() != CellType.BLANK) { - requestedClubMemberDtos.add(ClubMemberDto.fromExcelRow(row)); - } - } - } catch (IOException e) { - throw new IllegalArgumentException("μ˜¬λ°”λ₯Έ μ—‘μ…€ 양식을 μ‚¬μš©ν•΄μ£Όμ„Έμš”."); - } - return requestedClubMemberDtos; + @Transactional + public void deleteAll(List clubMembers) { + clubMemberRepository.deleteAllInBatch(clubMembers); } + } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubService.java b/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubService.java index 469c841b..550bd414 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/service/ClubService.java @@ -1,13 +1,15 @@ package ddingdong.ddingdongBE.domain.club.service; -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; -import static ddingdong.ddingdongBE.domain.club.entity.RecruitmentStatus.*; +import static ddingdong.ddingdongBE.domain.club.entity.RecruitmentStatus.BEFORE_RECRUIT; +import static ddingdong.ddingdongBE.domain.club.entity.RecruitmentStatus.END_RECRUIT; +import static ddingdong.ddingdongBE.domain.club.entity.RecruitmentStatus.RECRUITING; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.CLUB_INTRODUCE; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.CLUB_PROFILE; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.IMAGE; import ddingdong.ddingdongBE.auth.service.AuthService; -import ddingdong.ddingdongBE.domain.club.controller.dto.request.ClubMemberDto; +import ddingdong.ddingdongBE.common.exception.PersistenceException; +import ddingdong.ddingdongBE.domain.club.controller.dto.response.ClubMemberResponse; import ddingdong.ddingdongBE.domain.club.controller.dto.request.RegisterClubRequest; import ddingdong.ddingdongBE.domain.club.controller.dto.request.UpdateClubRequest; import ddingdong.ddingdongBE.domain.club.controller.dto.response.AdminClubResponse; @@ -15,23 +17,22 @@ import ddingdong.ddingdongBE.domain.club.controller.dto.response.DetailClubResponse; import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.club.entity.RecruitmentStatus; -import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; import ddingdong.ddingdongBE.domain.fileinformation.entity.FileInformation; import ddingdong.ddingdongBE.domain.fileinformation.repository.FileInformationRepository; import ddingdong.ddingdongBE.domain.fileinformation.service.FileInformationService; +import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; import ddingdong.ddingdongBE.domain.user.entity.User; +import ddingdong.ddingdongBE.file.FileStore; import java.time.LocalDateTime; import java.util.List; -import java.util.NoSuchElementException; -import ddingdong.ddingdongBE.file.FileStore; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -@Transactional +@Transactional(readOnly = true) public class ClubService { private final ClubRepository clubRepository; @@ -40,7 +41,8 @@ public class ClubService { private final FileStore fileStore; private final FileInformationRepository fileInformationRepository; - public Long register(RegisterClubRequest request) { + @Transactional + public Long create(RegisterClubRequest request) { User clubUser = authService.registerClubUser(request.getUserId(), request.getPassword(), request.getClubName()); Club club = request.toEntity(clubUser); @@ -49,24 +51,21 @@ public Long register(RegisterClubRequest request) { return savedClub.getId(); } - @Transactional(readOnly = true) - public List getAllClubs(LocalDateTime now) { + public List findAllWithRecruitTimeCheckPoint(LocalDateTime now) { return clubRepository.findAll().stream() .map(club -> ClubResponse.of(club, checkRecruit(now, club).getText())) .toList(); } - @Transactional(readOnly = true) - public List getAllForAdmin() { + public List findAllForAdmin() { return clubRepository.findAll().stream() .map(club -> AdminClubResponse.of(club, fileInformationService.getImageUrls( IMAGE.getFileType() + CLUB_PROFILE.getFileDomain() + club.getId()))) .toList(); } - @Transactional(readOnly = true) - public DetailClubResponse getClub(Long clubId) { - Club club = findClubByClubId(clubId); + public DetailClubResponse findByClubId(Long clubId) { + Club club = getByClubId(clubId); List profileImageUrl = fileInformationService.getImageUrls( IMAGE.getFileType() + CLUB_PROFILE.getFileDomain() + clubId); @@ -74,16 +73,15 @@ public DetailClubResponse getClub(Long clubId) { List introduceImageUrls = fileInformationService.getImageUrls( IMAGE.getFileType() + CLUB_INTRODUCE.getFileDomain() + clubId); - List clubMemberDtos = club.getClubMembers().stream() - .map(ClubMemberDto::from) + List clubMemberResponses = club.getClubMembers().stream() + .map(ClubMemberResponse::from) .toList(); - return DetailClubResponse.of(club, profileImageUrl, introduceImageUrls, clubMemberDtos); + return DetailClubResponse.of(club, profileImageUrl, introduceImageUrls, clubMemberResponses); } - @Transactional(readOnly = true) public DetailClubResponse getMyClub(Long userId) { - Club club = findClubByUserId(userId); + Club club = getByUserId(userId); List profileImageUrl = fileInformationService.getImageUrls( IMAGE.getFileType() + CLUB_PROFILE.getFileDomain() + club.getId()); @@ -91,27 +89,30 @@ public DetailClubResponse getMyClub(Long userId) { List introduceImageUrls = fileInformationService.getImageUrls( IMAGE.getFileType() + CLUB_INTRODUCE.getFileDomain() + club.getId()); - List clubMemberDtos = club.getClubMembers().stream() - .map(ClubMemberDto::from) + List clubMemberResponses = club.getClubMembers().stream() + .map(ClubMemberResponse::from) .toList(); - return DetailClubResponse.of(club, profileImageUrl, introduceImageUrls, clubMemberDtos); + return DetailClubResponse.of(club, profileImageUrl, introduceImageUrls, clubMemberResponses); } + @Transactional public void delete(Long clubId) { - Club club = findClubByClubId(clubId); + Club club = getByClubId(clubId); clubRepository.delete(club); } - public float editClubScore(Long clubId, float score) { - Club club = findClubByClubId(clubId); + @Transactional + public float updateClubScore(Long clubId, float score) { + Club club = getByClubId(clubId); return club.editScore(generateNewScore(club.getScore(), score)); } + @Transactional public Long update(Long userId, UpdateClubRequest request) { - Club club = findClubByUserId(userId); + Club club = getByUserId(userId); updateIntroduceImageInformation(request, club); updateProfileImageInformation(request, club); @@ -119,14 +120,14 @@ public Long update(Long userId, UpdateClubRequest request) { return club.getId(); } - public Club findClubByUserId(final Long userId) { + public Club getByUserId(final Long userId) { return clubRepository.findByUserId(userId) - .orElseThrow(() -> new NoSuchElementException(NO_SUCH_CLUB.getText())); + .orElseThrow(() -> new PersistenceException.ResourceNotFound("Club(userId=" + userId + "λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); } - public Club findClubByClubId(final Long clubId) { + public Club getByClubId(final Long clubId) { return clubRepository.findById(clubId) - .orElseThrow(() -> new NoSuchElementException(NO_SUCH_CLUB.getText())); + .orElseThrow(() -> new PersistenceException.ResourceNotFound("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ™μ•„λ¦¬μž…λ‹ˆλ‹€.")); } private void updateIntroduceImageInformation(UpdateClubRequest request, Club club) { @@ -164,7 +165,7 @@ private void updateProfileImageInformation(UpdateClubRequest request, Club club) } private Score generateNewScore(Score beforeUpdateScore, float value) { - return Score.of(beforeUpdateScore.getValue() + value); + return Score.from(beforeUpdateScore.getValue() + value); } private RecruitmentStatus checkRecruit(LocalDateTime now, Club club) { @@ -173,6 +174,7 @@ private RecruitmentStatus checkRecruit(LocalDateTime now, Club club) { return BEFORE_RECRUIT; } - return club.getEndRecruitPeriod().isAfter(now) ? RECRUITING : END_RECRUIT; + return club.getEndRecruitPeriod().isAfter(now) ? RECRUITING : END_RECRUIT; } + } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/service/FacadeClubMemberService.java b/src/main/java/ddingdong/ddingdongBE/domain/club/service/FacadeClubMemberService.java new file mode 100644 index 00000000..c7567607 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/service/FacadeClubMemberService.java @@ -0,0 +1,69 @@ +package ddingdong.ddingdongBE.domain.club.service; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.entity.ClubMember; +import ddingdong.ddingdongBE.domain.club.service.dto.UpdateClubMemberCommand; +import ddingdong.ddingdongBE.file.service.ExcelFileService; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@Service +@Transactional +@RequiredArgsConstructor +public class FacadeClubMemberService { + + private final ClubService clubService; + private final ClubMemberService clubMemberService; + private final ExcelFileService excelFileService; + + @Transactional(readOnly = true) + public byte[] getClubMemberListFile(Long userId) { + Club club = clubService.getByUserId(userId); + return excelFileService.generateClubMemberListFile(club.getClubMembers()); + } + + public void updateMemberList(Long userId, MultipartFile clubMemberListFile) { + Club club = clubService.getByUserId(userId); + List updatedClubMembers = excelFileService.extractClubMembersInformation(club, clubMemberListFile); + List clubMembers = club.getClubMembers(); + Set updatedMemberIds = updatedClubMembers.stream() + .map(ClubMember::getId) + .collect(Collectors.toSet()); + Set currentMemberIds = clubMembers.stream() + .map(ClubMember::getId) + .collect(Collectors.toSet()); + + clubMemberService.saveAll(filterCreatedMembers(updatedClubMembers, updatedMemberIds, currentMemberIds)); + clubMemberService.deleteAll(filterDeletedMembers(clubMembers, updatedMemberIds, currentMemberIds)); + } + + public void update(Long clubMemberId, UpdateClubMemberCommand updateClubMemberCommand) { + ClubMember clubMember = clubMemberService.getById(clubMemberId); + clubMember.updateInformation(updateClubMemberCommand.toEntity()); + } + + + private List filterCreatedMembers(List updatedClubMembers, Set updatedMemberIds, + Set currentMemberIds) { + Set createdMemberIds = new HashSet<>(updatedMemberIds); + createdMemberIds.removeAll(currentMemberIds); + return updatedClubMembers.stream() + .filter(member -> createdMemberIds.contains(member.getId())) + .toList(); + } + + private List filterDeletedMembers(List clubMembers, Set updatedMemberIds, + Set currentMemberIds) { + Set deletedMemberIds = new HashSet<>(currentMemberIds); + deletedMemberIds.removeAll(updatedMemberIds); + return clubMembers.stream() + .filter(member -> deletedMemberIds.contains(member.getId())) + .toList(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/club/service/dto/UpdateClubMemberCommand.java b/src/main/java/ddingdong/ddingdongBE/domain/club/service/dto/UpdateClubMemberCommand.java new file mode 100644 index 00000000..627d4161 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/club/service/dto/UpdateClubMemberCommand.java @@ -0,0 +1,26 @@ +package ddingdong.ddingdongBE.domain.club.service.dto; + +import ddingdong.ddingdongBE.domain.club.entity.ClubMember; +import ddingdong.ddingdongBE.domain.club.entity.Position; +import lombok.Builder; + +@Builder +public record UpdateClubMemberCommand( + String name, + String studentNumber, + String phoneNumber, + Position position, + String department +) { + + public ClubMember toEntity() { + return ClubMember.builder() + .name(name) + .studentNumber(studentNumber) + .phoneNumber(phoneNumber) + .position(position) + .department(department).build(); + } + + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/api/AdminDocumentApi.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/api/AdminDocumentApi.java new file mode 100644 index 00000000..75c5df45 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/api/AdminDocumentApi.java @@ -0,0 +1,65 @@ +package ddingdong.ddingdongBE.domain.documents.api; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.documents.controller.dto.request.GenerateDocumentRequest; +import ddingdong.ddingdongBE.domain.documents.controller.dto.request.ModifyDocumentRequest; +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.AdminDetailDocumentResponse; +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.AdminDocumentResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Document - Admin", description = "Document Admin API") +@RequestMapping("/server/admin/documents") +public interface AdminDocumentApi { + + @Operation(summary = "μ–΄λ“œλ―Ό μžλ£Œμ‹€ μ—…λ‘œλ“œ API") + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.CREATED) + @SecurityRequirement(name = "AccessToken") + void generateDocument( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @ModelAttribute GenerateDocumentRequest generateDocumentRequest, + @RequestPart(name = "uploadFiles") List uploadFiles + ); + + @Operation(summary = "μ–΄λ“œλ―Ό μžλ£Œμ‹€ λͺ©λ‘ 쑰회 API") + @GetMapping + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getAllDocuments(); + + @Operation(summary = "μ–΄λ“œλ―Ό μžλ£Œμ‹€ 상세 쑰회 API") + @GetMapping("/{documentId}") + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + AdminDetailDocumentResponse getDetailDocument(@PathVariable Long documentId); + + @Operation(summary = "μ–΄λ“œλ―Ό μžλ£Œμ‹€ μˆ˜μ • API") + @PatchMapping(value = "/{documentId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void modifyDocument(@PathVariable Long documentId, + @ModelAttribute ModifyDocumentRequest modifyDocumentRequest, + @RequestPart(name = "uploadFiles", required = false) List uploadFiles); + + @Operation(summary = "μ–΄λ“œλ―Ό μžλ£Œμ‹€ μ‚­μ œ API") + @DeleteMapping("/{documentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void deleteDocument(@PathVariable Long documentId); +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/api/DocumentApi.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/api/DocumentApi.java new file mode 100644 index 00000000..fbc0b524 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/api/DocumentApi.java @@ -0,0 +1,30 @@ +package ddingdong.ddingdongBE.domain.documents.api; + + +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.DetailDocumentResponse; +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.DocumentResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "Document", description = "Document API") +@RequestMapping("/server/documents") +public interface DocumentApi { + + @Operation(summary = "μžλ£Œμ‹€ λͺ©λ‘ 쑰회 API") + @GetMapping + @ResponseStatus(HttpStatus.OK) + List getAllDocuments(); + + @Operation(summary = "μžλ£Œμ‹€ 상세 쑰회 API") + @GetMapping("/{documentId}") + @ResponseStatus(HttpStatus.OK) + DetailDocumentResponse getDetailDocument(@PathVariable Long documentId); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/AdminDocumentController.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/AdminDocumentController.java new file mode 100644 index 00000000..644ed214 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/AdminDocumentController.java @@ -0,0 +1,69 @@ +package ddingdong.ddingdongBE.domain.documents.controller; + +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.DOCUMENT; +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.FILE; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.documents.api.AdminDocumentApi; +import ddingdong.ddingdongBE.domain.documents.controller.dto.request.GenerateDocumentRequest; +import ddingdong.ddingdongBE.domain.documents.controller.dto.request.ModifyDocumentRequest; +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.AdminDetailDocumentResponse; +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.AdminDocumentResponse; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.domain.documents.service.DocumentService; +import ddingdong.ddingdongBE.domain.fileinformation.service.FileInformationService; +import ddingdong.ddingdongBE.domain.user.entity.User; +import ddingdong.ddingdongBE.file.dto.FileResponse; +import ddingdong.ddingdongBE.file.service.FileService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequiredArgsConstructor +public class AdminDocumentController implements AdminDocumentApi { + + private final DocumentService documentService; + private final FileService fileService; + private final FileInformationService fileInformationService; + + public void generateDocument( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @ModelAttribute GenerateDocumentRequest generateDocumentRequest, + @RequestPart(name = "uploadFiles") List uploadFiles) { + User admin = principalDetails.getUser(); + Long createdDocumentId = documentService.create(generateDocumentRequest.toEntity(admin)); + fileService.uploadDownloadableFile(createdDocumentId, uploadFiles, FILE, DOCUMENT); + } + + public List getAllDocuments() { + return documentService.getAll().stream() + .map(AdminDocumentResponse::from) + .toList(); + } + + public AdminDetailDocumentResponse getDetailDocument(@PathVariable Long documentId) { + Document document = documentService.getById(documentId); + List fileResponse = fileInformationService.getFileUrls( + FILE.getFileType() + DOCUMENT.getFileDomain() + document.getId()); + return AdminDetailDocumentResponse.of(document, fileResponse); + } + + public void modifyDocument(@PathVariable Long documentId, + @ModelAttribute ModifyDocumentRequest modifyDocumentRequest, + @RequestPart(name = "uploadFiles", required = false) List uploadFiles) { + Long updateDocumentId = documentService.update(documentId, modifyDocumentRequest.toEntity()); + fileService.deleteFile(updateDocumentId, FILE, DOCUMENT); + fileService.uploadDownloadableFile(updateDocumentId, uploadFiles, FILE, DOCUMENT); + } + + public void deleteDocument(@PathVariable Long documentId) { + fileService.deleteFile(documentId, FILE, DOCUMENT); + documentService.delete(documentId); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/DocumentController.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/DocumentController.java new file mode 100644 index 00000000..109e261b --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/DocumentController.java @@ -0,0 +1,37 @@ +package ddingdong.ddingdongBE.domain.documents.controller; + +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.DOCUMENT; +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.FILE; + +import ddingdong.ddingdongBE.domain.documents.api.DocumentApi; +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.DetailDocumentResponse; +import ddingdong.ddingdongBE.domain.documents.controller.dto.response.DocumentResponse; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.domain.documents.service.DocumentService; +import ddingdong.ddingdongBE.domain.fileinformation.service.FileInformationService; +import ddingdong.ddingdongBE.file.dto.FileResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class DocumentController implements DocumentApi { + + private final DocumentService documentService; + private final FileInformationService fileInformationService; + + public List getAllDocuments() { + return documentService.getAll().stream() + .map(DocumentResponse::from) + .toList(); + } + + public DetailDocumentResponse getDetailDocument(@PathVariable Long documentId) { + Document document = documentService.getById(documentId); + List fileResponse = fileInformationService.getFileUrls( + FILE.getFileType() + DOCUMENT.getFileDomain() + document.getId()); + return DetailDocumentResponse.of(document, fileResponse); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/request/GenerateDocumentRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/request/GenerateDocumentRequest.java new file mode 100644 index 00000000..b8acece0 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/request/GenerateDocumentRequest.java @@ -0,0 +1,28 @@ +package ddingdong.ddingdongBE.domain.documents.controller.dto.request; + +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.domain.user.entity.User; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema( + name = "GenerateDocumentRequest", + description = "μžλ£Œμ‹€ 자료 생성 μš”μ²­" +) +@Builder +public record GenerateDocumentRequest( + @Schema(description = "자료 제λͺ©", example = "제λͺ©") + String title, + + @Schema(description = "자료 λ‚΄μš©", example = "λ‚΄μš©") + String content +) { + public Document toEntity(User user) { + return Document.builder() + .user(user) + .title(title) + .content(content) + .build(); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/request/ModifyDocumentRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/request/ModifyDocumentRequest.java new file mode 100644 index 00000000..4cdc4ef4 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/request/ModifyDocumentRequest.java @@ -0,0 +1,27 @@ +package ddingdong.ddingdongBE.domain.documents.controller.dto.request; + +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Schema( + name = "ModifyDocumentRequest", + description = "μžλ£Œμ‹€ 자료 μˆ˜μ • μš”μ²­" +) +@Builder +public record ModifyDocumentRequest( + @Schema(description = "자료 제λͺ©", example = "제λͺ©") + String title, + + @Schema(description = "자료 λ‚΄μš©", example = "λ‚΄μš©") String content +) { + + public Document toEntity() { + return Document.builder() + .title(title) + .content(content) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/AdminDetailDocumentResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/AdminDetailDocumentResponse.java new file mode 100644 index 00000000..4c72b796 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/AdminDetailDocumentResponse.java @@ -0,0 +1,41 @@ +package ddingdong.ddingdongBE.domain.documents.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.file.dto.FileResponse; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; + +@Schema( + name = "AdminDetailDocumentResponse", + description = "μ–΄λ“œλ―Ό μžλ£Œμ‹€ 자료 상세 쑰회 응닡" +) +@Builder +public record AdminDetailDocumentResponse( + @Schema(description = "자료 제λͺ©", example = "자료 제λͺ©") + String title, + + @Schema(description = "자료 λ‚΄μš©", example = "자료 λ‚΄μš©") + String content, + + @Schema(description = "μž‘μ„±μΌ", example = "2024-01-01") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate createdAt, + + @ArraySchema(schema = @Schema(description = "μ²¨λΆ€νŒŒμΌ λͺ©λ‘", implementation = FileResponse.class)) + List fileUrls +) { + + public static AdminDetailDocumentResponse of(Document document, + List fileResponses) { + return AdminDetailDocumentResponse.builder() + .title(document.getTitle()) + .content(document.getContent()) + .createdAt(document.getCreatedAt().toLocalDate()) + .fileUrls(fileResponses) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/AdminDocumentResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/AdminDocumentResponse.java new file mode 100644 index 00000000..d483e285 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/AdminDocumentResponse.java @@ -0,0 +1,33 @@ +package ddingdong.ddingdongBE.domain.documents.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import lombok.Builder; + +@Schema( + name = "AdminDocumentResponse", + description = "μ–΄λ“œλ―Ό μžλ£Œμ‹€ 자료 λͺ©λ‘ 쑰회 응닡" +) +@Builder +public record AdminDocumentResponse( + @Schema(description = "자료 μ‹λ³„μž", example = "1") + Long id, + + @Schema(description = "자료 제λͺ©", example = "자료 제λͺ©") + String title, + + @Schema(description = "μž‘μ„±μΌ", example = "2024-01-01") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate createdAt +) { + + public static AdminDocumentResponse from(Document document) { + return AdminDocumentResponse.builder() + .id(document.getId()) + .title(document.getTitle()) + .createdAt(document.getCreatedAt().toLocalDate()) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/DetailDocumentResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/DetailDocumentResponse.java new file mode 100644 index 00000000..dec6995d --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/DetailDocumentResponse.java @@ -0,0 +1,41 @@ +package ddingdong.ddingdongBE.domain.documents.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.file.dto.FileResponse; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; + +@Schema( + name = "DetailDocumentResponse", + description = "μžλ£Œμ‹€ 자료 상세 쑰회 응닡" +) +@Builder +public record DetailDocumentResponse( + @Schema(description = "자료 제λͺ©", example = "자료 제λͺ©") + String title, + + @Schema(description = "자료 λ‚΄μš©", example = "자료 λ‚΄μš©") + String content, + + @Schema(description = "μž‘μ„±μΌ", example = "2024-01-01") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate createdAt, + + @ArraySchema(schema = @Schema(description = "μ²¨λΆ€νŒŒμΌ λͺ©λ‘", implementation = FileResponse.class)) + List fileUrls +) { + + public static DetailDocumentResponse of(Document document, + List fileResponses) { + return DetailDocumentResponse.builder() + .title(document.getTitle()) + .content(document.getContent()) + .createdAt(document.getCreatedAt().toLocalDate()) + .fileUrls(fileResponses) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/DocumentResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/DocumentResponse.java new file mode 100644 index 00000000..cc33894c --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/controller/dto/response/DocumentResponse.java @@ -0,0 +1,34 @@ +package ddingdong.ddingdongBE.domain.documents.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import lombok.Builder; + +@Schema( + name = "DocumentResponse", + description = "μžλ£Œμ‹€ 자료 λͺ©λ‘ 쑰회 응닡" +) +@Builder +public record DocumentResponse( + @Schema(description = "자료 μ‹λ³„μž", example = "1") + Long id, + + @Schema(description = "자료 제λͺ©", example = "자료 제λͺ©") + String title, + + @Schema(description = "μž‘μ„±μΌ", example = "2024-01-01") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate createdAt +) { + + public static DocumentResponse from(Document document) { + return DocumentResponse.builder() + .id(document.getId()) + .title(document.getTitle()) + .createdAt(document.getCreatedAt().toLocalDate()) + .build(); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/entity/Document.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/entity/Document.java new file mode 100644 index 00000000..844d429f --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/entity/Document.java @@ -0,0 +1,61 @@ +package ddingdong.ddingdongBE.domain.documents.entity; + +import ddingdong.ddingdongBE.common.BaseEntity; +import ddingdong.ddingdongBE.domain.user.entity.User; +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update document set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "document") +public class Document extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false) + private String title; + + @Column(nullable = false, length = 1024) + private String content; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + + @Builder + private Document(Long id, User user, String title, String content, LocalDateTime createdAt) { + this.id = id; + this.user = user; + this.title = title; + this.content = content; + super.setCreatedAt(createdAt); + } + + public void updateDocument(Document updatedDocument) { + this.title = updatedDocument.getTitle(); + this.content = updatedDocument.getContent(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/repository/DocumentRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/repository/DocumentRepository.java new file mode 100644 index 00000000..e4343104 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/repository/DocumentRepository.java @@ -0,0 +1,8 @@ +package ddingdong.ddingdongBE.domain.documents.repository; + +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DocumentRepository extends JpaRepository { + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/documents/service/DocumentService.java b/src/main/java/ddingdong/ddingdongBE/domain/documents/service/DocumentService.java new file mode 100644 index 00000000..68db381d --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/documents/service/DocumentService.java @@ -0,0 +1,51 @@ +package ddingdong.ddingdongBE.domain.documents.service; + +import static ddingdong.ddingdongBE.common.exception.ErrorMessage.NO_SUCH_DOCUMENT; + +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.domain.documents.repository.DocumentRepository; +import java.util.List; +import java.util.NoSuchElementException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class DocumentService { + + private final DocumentRepository documentRepository; + + public Long create(Document document) { + Document createdDocument = documentRepository.save(document); + return createdDocument.getId(); + } + + @Transactional(readOnly = true) + public List getAll() { + return documentRepository.findAll(); + } + + @Transactional(readOnly = true) + public Document getById(Long documentId) { + return getDocument(documentId); + } + + public Long update(Long documentId, Document updatedDocument) { + Document document = getDocument(documentId); + document.updateDocument(updatedDocument); + return document.getId(); + } + + public void delete(Long documentId) { + Document document = getDocument(documentId); + documentRepository.delete(document); + } + + private Document getDocument(Long documentId) { + return documentRepository.findById(documentId) + .orElseThrow(() -> new NoSuchElementException(NO_SUCH_DOCUMENT.getText())); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileDomainCategory.java b/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileDomainCategory.java index bc1cdb61..f3196acb 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileDomainCategory.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileDomainCategory.java @@ -13,7 +13,8 @@ public enum FileDomainCategory { BANNER("banner/"), ACTIVITY_REPORT("activity-report/"), FIX_ZONE("fix/"), - EVENT("event/"); + EVENT("event/"), + DOCUMENT("document/"); private final String fileDomain; } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileInformation.java b/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileInformation.java index 91d18773..d416d6e2 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileInformation.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fileinformation/entity/FileInformation.java @@ -1,6 +1,8 @@ package ddingdong.ddingdongBE.domain.fileinformation.entity; import ddingdong.ddingdongBE.common.BaseEntity; +import java.time.LocalDateTime; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -11,10 +13,16 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update file_information set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "file_information") public class FileInformation extends BaseEntity { @Id @@ -33,6 +41,10 @@ public class FileInformation extends BaseEntity { private String findParam; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Builder public FileInformation(String uploadName, String storedName, FileTypeCategory fileTypeCategory, FileDomainCategory fileDomainCategory, String findParam) { diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/AdminFixZoneController.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/AdminFixZoneController.java index fba7d8d5..ba5c5d00 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/AdminFixZoneController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/AdminFixZoneController.java @@ -1,41 +1,69 @@ package ddingdong.ddingdongBE.domain.fixzone.controller; -import java.util.List; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFiXCompletionRequest; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFixRequest; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.AdminDetailFixResponse; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.AdminFixResponse; +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.service.ClubService; +import ddingdong.ddingdongBE.domain.fixzone.controller.api.AdminFixZoneApi; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixZoneCommentRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetFixZoneResponse; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZone; +import ddingdong.ddingdongBE.domain.fixzone.service.FixZoneCommentService; import ddingdong.ddingdongBE.domain.fixzone.service.FixZoneService; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/server/admin/fix") @RequiredArgsConstructor -public class AdminFixZoneController { +public class AdminFixZoneController implements AdminFixZoneApi { private final FixZoneService fixZoneService; + private final FixZoneCommentService fixZoneCommentService; + private final ClubService clubService; + + @Override + public List getFixZones() { + return fixZoneService.getAll(); + } + + @Override + public void updateFixZoneToComplete(Long fixZoneId) { + fixZoneService.updateToComplete(fixZoneId); + } + + @Override + public void createFixZoneComment( + PrincipalDetails principalDetails, + CreateFixZoneCommentRequest request, + Long fixZoneId + ) { + FixZone fixZone = fixZoneService.getById(fixZoneId); + Club club = clubService.getByClubId(principalDetails.getUser().getId()); + + fixZoneCommentService.create(fixZone, club, request); + } + + @Override + public void updateFixZoneComment( + PrincipalDetails principalDetails, + CreateFixZoneCommentRequest request, + Long fixZoneId, + Long commentId + ) { + Club club = clubService.getByClubId(principalDetails.getUser().getId()); + + fixZoneCommentService.update(club.getId(), commentId, request); + } - @GetMapping - public List getAllFixForAdmin() { - return fixZoneService.getAllForAdmin(); - } + @Override + public void deleteFixZoneComment( + PrincipalDetails principalDetails, + Long fixZoneId, + Long commentId + ) { + Club club = clubService.getByClubId(principalDetails.getUser().getId()); - @GetMapping("/{fixId}") - public AdminDetailFixResponse getFixForAdmin(@PathVariable Long fixId) { - return fixZoneService.getForAdmin(fixId); - } + fixZoneCommentService.delete(commentId); + } - @PatchMapping("/{fixId}") - public void updateFix(@PathVariable Long fixId, @RequestBody UpdateFiXCompletionRequest request) { - fixZoneService.updateIsCompleted(fixId, request); - } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/ClubFixZoneController.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/ClubFixZoneController.java index 99abcc0a..e9cad21f 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/ClubFixZoneController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/ClubFixZoneController.java @@ -1,77 +1,79 @@ package ddingdong.ddingdongBE.domain.fixzone.controller; -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; -import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.*; -import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.*; - -import java.util.List; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.FIX_ZONE; +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.IMAGE; import ddingdong.ddingdongBE.auth.PrincipalDetails; import ddingdong.ddingdongBE.domain.club.entity.Club; -import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixRequest; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFixRequest; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.ClubDetailFixResponse; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.ClubFixResponse; +import ddingdong.ddingdongBE.domain.club.service.ClubService; +import ddingdong.ddingdongBE.domain.fixzone.controller.api.ClubFixZoneApi; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixZoneRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFixZoneRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetDetailFixZoneResponse; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetFixZoneResponse; import ddingdong.ddingdongBE.domain.fixzone.service.FixZoneService; import ddingdong.ddingdongBE.file.service.FileService; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController -@RequestMapping("/server/club/fix") @RequiredArgsConstructor -public class ClubFixZoneController { +public class ClubFixZoneController implements ClubFixZoneApi { + private final ClubService clubService; + private final FixZoneService fixZoneService; + private final FileService fileService; + + @Override + public List getMyFixZones(PrincipalDetails principalDetails) { + Club club = clubService.getByUserId(principalDetails.getUser().getId()); + + return fixZoneService.getMyFixZones(club.getId()); + } + + @Override + public GetDetailFixZoneResponse getFixZoneDetail(Long fixZoneId) { + return fixZoneService.getFixZoneDetail(fixZoneId); + } - private final ClubRepository clubRepository; - private final FixZoneService fixZoneService; - private final FileService fileService; + @Override + public void createFixZone( + PrincipalDetails principalDetails, + CreateFixZoneRequest request, + List images + ) { + Club club = clubService.getByUserId(principalDetails.getUser().getId()); + Long createdFixZoneId = fixZoneService.create(club, request); - @PostMapping - public void createFix(@AuthenticationPrincipal PrincipalDetails principalDetails, - @ModelAttribute CreateFixRequest request, - @RequestPart(name = "images") List images) { - Club club = clubRepository.findByUserId(principalDetails.getUser().getId()) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_CLUB.getText())); - Long createdFixId = fixZoneService.create(club, request); + fileService.uploadFile(createdFixZoneId, images, IMAGE, FIX_ZONE); + } - fileService.uploadFile(createdFixId, images, IMAGE, FIX_ZONE); - } + @Override + public void updateFixZone( + PrincipalDetails principalDetails, + Long fixZoneId, + UpdateFixZoneRequest request, + List images + ) { + clubService.getByUserId(principalDetails.getUser().getId()); - @GetMapping - public List findAllFixForClub() { - return fixZoneService.getAllForClub(); - } + fixZoneService.update(fixZoneId, request); - @GetMapping("{fixId}") - public ClubDetailFixResponse findFixForClub(@PathVariable Long fixId) { - return fixZoneService.getForClub(fixId); - } + fileService.deleteFile(fixZoneId, IMAGE, FIX_ZONE); + fileService.uploadFile(fixZoneId, images, IMAGE, FIX_ZONE); + } - @PatchMapping("/{fixId}") - public void updateFix(@PathVariable Long fixId, @ModelAttribute UpdateFixRequest request, - @RequestPart(name = "images") List images) { - fixZoneService.update(fixId, request); + @Override + public void deleteFixZone( + PrincipalDetails principalDetails, + Long fixZoneId + ) { + clubService.getByUserId(principalDetails.getUser().getId()); - fileService.deleteFile(fixId, IMAGE, FIX_ZONE); - fileService.uploadFile(fixId, images, IMAGE, FIX_ZONE); - } + fixZoneService.delete(fixZoneId); - @DeleteMapping("{fixId}") - public void deleteFix(@PathVariable Long fixId) { - fixZoneService.delete(fixId); + fileService.deleteFile(fixZoneId, IMAGE, FIX_ZONE); + } - fileService.deleteFile(fixId, IMAGE, FIX_ZONE); - } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/api/AdminFixZoneApi.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/api/AdminFixZoneApi.java new file mode 100644 index 00000000..af0e9df4 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/api/AdminFixZoneApi.java @@ -0,0 +1,70 @@ +package ddingdong.ddingdongBE.domain.fixzone.controller.api; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixZoneCommentRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetFixZoneResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "Fix Zone - Admin", description = "Fix Zone Admin API") +@RequestMapping(value = "/server/admin/fix-zones", produces = APPLICATION_JSON_VALUE) +public interface AdminFixZoneApi { + + @Operation(summary = "Fix Zone 전체 쑰회 API") + @GetMapping + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getFixZones(); + + @Operation(summary = "Fix Zone μš”μ²­ 처리 μ™„λ£Œ API") + @PatchMapping("/{fixZoneId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void updateFixZoneToComplete(@PathVariable("fixZoneId") Long fixZoneId); + + @Operation(summary = "Fix Zone λŒ“κΈ€ 등둝 API") + @PostMapping("/{fixZoneId}/comments") + @ResponseStatus(HttpStatus.CREATED) + @SecurityRequirement(name = "AccessToken") + void createFixZoneComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody CreateFixZoneCommentRequest request, + @PathVariable("fixZoneId") Long fixZoneId + ); + + @Operation(summary = "Fix Zone λŒ“κΈ€ μˆ˜μ • API") + @PatchMapping("/{fixZoneId}/comments/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void updateFixZoneComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody CreateFixZoneCommentRequest request, + @PathVariable("fixZoneId") Long fixZoneId, + @PathVariable("commentId") Long commentId + ); + + @Operation(summary = "Fix Zone λŒ“κΈ€ μ‚­μ œ API") + @DeleteMapping("/{fixZoneId}/comments/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void deleteFixZoneComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("fixZoneId") Long fixZoneId, + @PathVariable("commentId") Long commentId + ); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/api/ClubFixZoneApi.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/api/ClubFixZoneApi.java new file mode 100644 index 00000000..2e888eea --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/api/ClubFixZoneApi.java @@ -0,0 +1,74 @@ +package ddingdong.ddingdongBE.domain.fixzone.controller.api; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixZoneRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFixZoneRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetDetailFixZoneResponse; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetFixZoneResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Fix Zone - Club", description = "Fix Zone Club API") +@RequestMapping(value = "/server/club/fix-zones", produces = APPLICATION_JSON_VALUE) +public interface ClubFixZoneApi { + + @Operation(summary = "ν΄λŸ½λ³„ λ“±λ‘ν•œ Fix Zone 쑰회") + @GetMapping + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getMyFixZones(@AuthenticationPrincipal PrincipalDetails principalDetails); + + @Operation(summary = "Fix Zone 상세 쑰회") + @GetMapping("/{fixZoneId}") + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + GetDetailFixZoneResponse getFixZoneDetail(@PathVariable("fixZoneId") Long fixZoneId); + + @Operation(summary = "Fix Zone 등둝 API") + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.CREATED) + @SecurityRequirement(name = "AccessToken") + void createFixZone( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @ModelAttribute CreateFixZoneRequest request, + @RequestPart(name = "images", required = false) List images + ); + + @Operation(summary = "Fix Zone μˆ˜μ • API") + @PatchMapping(value = "/{fixZoneId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void updateFixZone( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("fixZoneId") Long fixZoneId, + @ModelAttribute UpdateFixZoneRequest request, + @RequestPart(name = "images", required = false) List images + ); + + @Operation(summary = "Fix Zone μ‚­μ œ API") + @DeleteMapping("/{fixZoneId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void deleteFixZone( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("fixZoneId") Long fixZoneId + ); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixZoneCommentRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixZoneCommentRequest.java new file mode 100644 index 00000000..374ec0cc --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixZoneCommentRequest.java @@ -0,0 +1,23 @@ +package ddingdong.ddingdongBE.domain.fixzone.controller.dto.request; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZone; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZoneComment; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "CreateFixZoneCommentRequest", description = "Admin - ν”½μŠ€μ‘΄ λŒ“κΈ€ 등둝 μš”μ²­") +public record CreateFixZoneCommentRequest( + @Schema(description = "λŒ“κΈ€ λ‚΄μš©") + String content +) { + + public FixZoneComment toEntity(FixZone fixZone, Club club) { + return FixZoneComment.builder() + .fixZone(fixZone) + .club(club) + .content(content) + .build(); + } + +} + diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixZoneRequest.java similarity index 68% rename from src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixRequest.java rename to src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixZoneRequest.java index becf8580..e9cfd422 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixRequest.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/CreateFixZoneRequest.java @@ -1,20 +1,20 @@ package ddingdong.ddingdongBE.domain.fixzone.controller.dto.request; import ddingdong.ddingdongBE.domain.club.entity.Club; -import ddingdong.ddingdongBE.domain.fixzone.entitiy.Fix; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZone; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor -public class CreateFixRequest { +public class CreateFixZoneRequest { private String title; private String content; - public Fix toEntity(Club club) { - return Fix.builder() + public FixZone toEntity(Club club) { + return FixZone.builder() .title(title) .content(content) .club(club) diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneCommentRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneCommentRequest.java new file mode 100644 index 00000000..2c7e367e --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneCommentRequest.java @@ -0,0 +1,10 @@ +package ddingdong.ddingdongBE.domain.fixzone.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "UpdateFixZoneCommentRequest", description = "Admin - ν”½μŠ€μ‘΄ λŒ“κΈ€ μˆ˜μ • μš”μ²­") +public record UpdateFixZoneCommentRequest( + @Schema(description = "μˆ˜μ •ν•  λŒ“κΈ€ λ‚΄μš©") + String content +) { +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneRequest.java similarity index 78% rename from src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixRequest.java rename to src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneRequest.java index f029c0d5..b905b6fb 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixRequest.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneRequest.java @@ -5,11 +5,10 @@ @Getter @AllArgsConstructor -public class UpdateFixRequest { +public class UpdateFixZoneRequest { private String title; private String content; - private String imgUrls; } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFiXCompletionRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneStatusRequest.java similarity index 75% rename from src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFiXCompletionRequest.java rename to src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneStatusRequest.java index 73f27d40..4c52aaa5 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFiXCompletionRequest.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/request/UpdateFixZoneStatusRequest.java @@ -3,7 +3,8 @@ import lombok.Getter; @Getter -public class UpdateFiXCompletionRequest { +public class UpdateFixZoneStatusRequest { private boolean completed; + } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/AdminDetailFixResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/AdminDetailFixResponse.java deleted file mode 100644 index 17efe8c2..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/AdminDetailFixResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -package ddingdong.ddingdongBE.domain.fixzone.controller.dto.response; - -import java.time.LocalDateTime; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class AdminDetailFixResponse { - - private Long id; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") - private LocalDateTime createdAt; - - private String club; - - private String location; - - private String title; - - private String content; - - private boolean isCompleted; - - private List imageUrls; - - @Builder - public AdminDetailFixResponse(Long id, LocalDateTime createdAt, String club, String location, String title, - String content, boolean isCompleted, List imageUrls) { - this.id = id; - this.createdAt = createdAt; - this.club = club; - this.location = location; - this.title = title; - this.content = content; - this.isCompleted = isCompleted; - this.imageUrls = imageUrls; - } -} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/AdminFixResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/AdminFixResponse.java deleted file mode 100644 index 8342e8d8..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/AdminFixResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -package ddingdong.ddingdongBE.domain.fixzone.controller.dto.response; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import ddingdong.ddingdongBE.domain.fixzone.entitiy.Fix; -import lombok.Builder; -import lombok.Getter; - -@Getter -public class AdminFixResponse { - - private Long id; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") - private LocalDateTime createdAt; - - private String club; - - private String title; - - private boolean isCompleted; - - @Builder - public AdminFixResponse(Long id, LocalDateTime createdAt, String club, String title, boolean isCompleted) { - this.id = id; - this.createdAt = createdAt; - this.club = club; - this.title = title; - this.isCompleted = isCompleted; - } - - public static AdminFixResponse from(Fix fix) { - return AdminFixResponse.builder() - .id(fix.getId()) - .createdAt(fix.getCreatedAt()) - .club(fix.getClub().getName()) - .title(fix.getTitle()) - .isCompleted(fix.isCompleted()).build(); - } -} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/ClubDetailFixResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/ClubDetailFixResponse.java deleted file mode 100644 index 428bfefd..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/ClubDetailFixResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package ddingdong.ddingdongBE.domain.fixzone.controller.dto.response; - -import java.time.LocalDateTime; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class ClubDetailFixResponse { - - private Long id; - - private String title; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") - private LocalDateTime createdAt; - - private String content; - - private boolean isCompleted; - - private List imageUrls; - - @Builder - public ClubDetailFixResponse(Long id, String title, LocalDateTime createdAt, String content, boolean isCompleted, - List imageUrls) { - this.id = id; - this.title = title; - this.createdAt = createdAt; - this.content = content; - this.isCompleted = isCompleted; - this.imageUrls = imageUrls; - } -} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/ClubFixResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/ClubFixResponse.java deleted file mode 100644 index 51c79c5d..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/ClubFixResponse.java +++ /dev/null @@ -1,39 +0,0 @@ -package ddingdong.ddingdongBE.domain.fixzone.controller.dto.response; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import ddingdong.ddingdongBE.domain.fixzone.entitiy.Fix; -import lombok.Builder; -import lombok.Getter; - -@Getter -public class ClubFixResponse { - - private Long id; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") - private LocalDateTime createdAt; - - private String title; - - private boolean isCompleted; - - @Builder - public ClubFixResponse(Long id, String title, LocalDateTime createdAt, boolean isCompleted) { - this.id = id; - this.createdAt = createdAt; - this.title = title; - this.isCompleted = isCompleted; - } - - public static ClubFixResponse from(Fix fix) { - return ClubFixResponse.builder() - .id(fix.getId()) - .createdAt(fix.getCreatedAt()) - .title(fix.getTitle()) - .isCompleted(fix.isCompleted()).build(); - } - -} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetDetailFixZoneResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetDetailFixZoneResponse.java new file mode 100644 index 00000000..54fb340f --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetDetailFixZoneResponse.java @@ -0,0 +1,64 @@ +package ddingdong.ddingdongBE.domain.fixzone.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "상세 FixZone 응닡") +public record GetDetailFixZoneResponse( + + @Schema(description = "ν”½μŠ€μ‘΄ id") + Long id, + + @Schema(description = "동아리 μœ„μΉ˜") + String clubLocation, + + @Schema(description = "동아리λͺ…") + String clubName, + + @Schema(description = "제λͺ©") + String title, + + @Schema(description = "λ‚΄μš©") + String content, + + @Schema(description = "ν”½μŠ€μ‘΄ μ™„λ£Œ 처리 μ—¬λΆ€") + boolean isCompleted, + + @Schema(description = "μš”μ²­ μ‹œκ°", pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime requestedAt, + + @Schema(description = "이미지 URL λͺ©λ‘") + List imageUrls, + + @Schema(description = "Fix Zone λŒ“κΈ€ λͺ©λ‘") + List comments +) { + + public static GetDetailFixZoneResponse of( + Long id, + String clubLocation, + String clubName, + String title, + LocalDateTime createdAt, + String content, + boolean isCompleted, + List imageUrls, + List comments + ) { + return new GetDetailFixZoneResponse( + id, + clubLocation, + clubName, + title, + content, + isCompleted, + createdAt, + imageUrls, + comments + ); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetFixZoneCommentResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetFixZoneCommentResponse.java new file mode 100644 index 00000000..79537123 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetFixZoneCommentResponse.java @@ -0,0 +1,41 @@ +package ddingdong.ddingdongBE.domain.fixzone.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZoneComment; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(name = "GetFixZoneCommentResponse", description = "Fix Zone Comment 응닡") +public record GetFixZoneCommentResponse( + @Schema(description = "λŒ“κΈ€ id") + Long commentId, + + @Schema(description = "λŒ“κΈ€ μž‘μ„±μž") + String commentor, + + @Schema(description = "λŒ“κΈ€ λ‚΄μš©") + String content, + + @Schema(description = "μž‘μ„±μž ν”„λ‘œν•„ 이미지 URL") + String profileImageUrl, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + @Schema(description = "λŒ“κΈ€ μž‘μ„± μ‹œκ°", example = "2024-07-30 22:30:00") + LocalDateTime createdAt +) { + + public static GetFixZoneCommentResponse of( + FixZoneComment comment, + String profileImageUrl + ) { + return new GetFixZoneCommentResponse( + comment.getId(), + comment.getClub().getName(), + comment.getContent(), + profileImageUrl, + comment.getCreatedAt() + ); + } + +} + diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetFixZoneResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetFixZoneResponse.java new file mode 100644 index 00000000..3bd2b00e --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/controller/dto/response/GetFixZoneResponse.java @@ -0,0 +1,43 @@ +package ddingdong.ddingdongBE.domain.fixzone.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZone; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(name = "ClubGetFixZoneResponse", description = "Club - ν”½μŠ€μ‘΄ 쑰회 응닡") +public record GetFixZoneResponse( + + @Schema(description = "Fix zone ID") + Long fixZoneId, + + @Schema(description = "동아리방 μœ„μΉ˜") + String clubLocation, + + @Schema(description = "클럽λͺ…") + String clubName, + + @Schema(description = "제λͺ©") + String title, + + @Schema(description = "처리 μ™„λ£Œ μ—¬λΆ€") + boolean isCompleted, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "μš”μ²­ μ‹œκ°", example = "2023-07-23 14:55:00") + LocalDateTime requestedAt +) { + + public static GetFixZoneResponse of(FixZone fixZone) { + return new GetFixZoneResponse( + fixZone.getId(), + fixZone.getClub().getLocation().getValue(), + fixZone.getClub().getName(), + fixZone.getTitle(), + fixZone.isCompleted(), + fixZone.getCreatedAt() + ); + } + +} + diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entitiy/Fix.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entitiy/Fix.java deleted file mode 100644 index 7683dbf9..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entitiy/Fix.java +++ /dev/null @@ -1,56 +0,0 @@ -package ddingdong.ddingdongBE.domain.fixzone.entitiy; - -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; - -import ddingdong.ddingdongBE.common.BaseEntity; -import ddingdong.ddingdongBE.domain.club.entity.Club; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFixRequest; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Fix extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "club_id") - private Club club; - - private String title; - - private String content; - - private boolean isCompleted; - - @Builder - private Fix(Long id, Club club, String title, String content, boolean isCompleted) { - this.id = id; - this.club = club; - this.title = title; - this.content = content; - this.isCompleted = isCompleted; - } - - public void update(UpdateFixRequest request) { - this.title = request.getTitle() != null ? request.getTitle() : this.title; - this.content = request.getContent() != null ? request.getContent() : this.content; - } - - public void updateIsCompleted(boolean isCompleted) { - this.isCompleted = isCompleted; - } -} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entity/FixZone.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entity/FixZone.java new file mode 100644 index 00000000..2b640a30 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entity/FixZone.java @@ -0,0 +1,72 @@ +package ddingdong.ddingdongBE.domain.fixzone.entity; + +import ddingdong.ddingdongBE.common.BaseEntity; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Table; +import org.hibernate.annotations.Where; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update fix_zone set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(appliesTo = "fix_zone") +public class FixZone extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "club_id") + private Club club; + + @OneToMany(mappedBy = "fixZone", cascade = CascadeType.ALL, orphanRemoval = true) + private List fixZoneComments = new ArrayList<>(); + + private String title; + + private String content; + + private boolean isCompleted; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Builder + private FixZone(Long id, Club club, String title, String content, boolean isCompleted) { + this.id = id; + this.club = club; + this.title = title; + this.content = content; + this.isCompleted = isCompleted; + } + + public void update(String title, String content) { + this.title = title; + this.content = content; + } + + public void updateToComplete() { + this.isCompleted = true; + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entity/FixZoneComment.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entity/FixZoneComment.java new file mode 100644 index 00000000..8861aac8 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/entity/FixZoneComment.java @@ -0,0 +1,62 @@ +package ddingdong.ddingdongBE.domain.fixzone.entity; + +import ddingdong.ddingdongBE.common.BaseEntity; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import java.time.LocalDateTime; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Entity +@Getter +@SQLDelete(sql = "update fix_zone_comment set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "fix_zone_comment") +public class FixZoneComment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "club_id") + private Club club; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fix_zone_id", nullable = false) + private FixZone fixZone; + + private String content; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Builder + public FixZoneComment(Long id, Club club, FixZone fixZone, String content) { + this.id = id; + this.club = club; + this.fixZone = fixZone; + this.content = content; + } + + public void update(Long clubId, String content) { + if (Objects.equals(clubId, this.club.getId())) { + this.content = content; + } + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixZoneCommentRepository.java similarity index 54% rename from src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixRepository.java rename to src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixZoneCommentRepository.java index 0e229bdd..fbe8366e 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixRepository.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixZoneCommentRepository.java @@ -1,10 +1,9 @@ package ddingdong.ddingdongBE.domain.fixzone.repository; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZoneComment; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import ddingdong.ddingdongBE.domain.fixzone.entitiy.Fix; - @Repository -public interface FixRepository extends JpaRepository { +public interface FixZoneCommentRepository extends JpaRepository { } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixZoneRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixZoneRepository.java new file mode 100644 index 00000000..5c1c6acd --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/repository/FixZoneRepository.java @@ -0,0 +1,13 @@ +package ddingdong.ddingdongBE.domain.fixzone.repository; + +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZone; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FixZoneRepository extends JpaRepository { + + List findAllByClubId(Long clubId); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/service/FixZoneCommentService.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/service/FixZoneCommentService.java new file mode 100644 index 00000000..4f553c96 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/service/FixZoneCommentService.java @@ -0,0 +1,45 @@ +package ddingdong.ddingdongBE.domain.fixzone.service; + +import static ddingdong.ddingdongBE.common.exception.ErrorMessage.NO_SUCH_FIX_ZONE_COMMENT; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixZoneCommentRequest; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZone; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZoneComment; +import ddingdong.ddingdongBE.domain.fixzone.repository.FixZoneCommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FixZoneCommentService { + + private final FixZoneCommentRepository fixZoneCommentRepository; + + @Transactional + public void create(FixZone fixZone, Club club, CreateFixZoneCommentRequest request) { + fixZoneCommentRepository.save(request.toEntity(fixZone, club)); + } + + @Transactional + public void update(Long clubId, Long commentId, CreateFixZoneCommentRequest request) { + FixZoneComment fixZoneComment = getById(commentId); + + fixZoneComment.update(clubId, request.content()); + } + + @Transactional + public void delete(Long commentId) { + FixZoneComment fixZoneComment = getById(commentId); + + fixZoneCommentRepository.delete(fixZoneComment); + } + + public FixZoneComment getById(Long commentId) { + return fixZoneCommentRepository.findById(commentId) + .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX_ZONE_COMMENT.getText())); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/service/FixZoneService.java b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/service/FixZoneService.java index c13db55e..da65880f 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/fixzone/service/FixZoneService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/fixzone/service/FixZoneService.java @@ -1,104 +1,114 @@ package ddingdong.ddingdongBE.domain.fixzone.service; -import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; +import static ddingdong.ddingdongBE.common.exception.ErrorMessage.NO_SUCH_FIX; +import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.CLUB_PROFILE; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileDomainCategory.FIX_ZONE; import static ddingdong.ddingdongBE.domain.fileinformation.entity.FileTypeCategory.IMAGE; -import java.util.List; - -import javax.transaction.Transactional; - -import org.springframework.stereotype.Service; - import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.fileinformation.service.FileInformationService; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixRequest; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFiXCompletionRequest; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFixRequest; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.AdminDetailFixResponse; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.AdminFixResponse; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.ClubDetailFixResponse; -import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.ClubFixResponse; -import ddingdong.ddingdongBE.domain.fixzone.entitiy.Fix; -import ddingdong.ddingdongBE.domain.fixzone.repository.FixRepository; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.CreateFixZoneRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.request.UpdateFixZoneRequest; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetDetailFixZoneResponse; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetFixZoneCommentResponse; +import ddingdong.ddingdongBE.domain.fixzone.controller.dto.response.GetFixZoneResponse; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZone; +import ddingdong.ddingdongBE.domain.fixzone.entity.FixZoneComment; +import ddingdong.ddingdongBE.domain.fixzone.repository.FixZoneRepository; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service -@Transactional +@Transactional(readOnly = true) @RequiredArgsConstructor public class FixZoneService { - private final FixRepository fixRepository; - private final FileInformationService fileInformationService; - - public Long create(Club club, CreateFixRequest request) { - Fix createdFix = request.toEntity(club); - Fix savedFix = fixRepository.save(createdFix); - return savedFix.getId(); - } - - public List getAllForClub() { - return fixRepository.findAll().stream() - .map(ClubFixResponse::from) - .toList(); - } - - public ClubDetailFixResponse getForClub(Long fixId) { - Fix fix = fixRepository.findById(fixId) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX.getText())); - - List imageUrls = fileInformationService.getImageUrls( - IMAGE.getFileType() + FIX_ZONE.getFileDomain() + fix.getId()); - - return ClubDetailFixResponse.builder() - .id(fix.getId()) - .title(fix.getTitle()) - .createdAt(fix.getCreatedAt()) - .content(fix.getContent()) - .imageUrls(imageUrls).build(); - } - - public List getAllForAdmin() { - return fixRepository.findAll().stream() - .map(AdminFixResponse::from) - .toList(); - } - - public AdminDetailFixResponse getForAdmin(Long fixId) { - Fix fix = fixRepository.findById(fixId) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX.getText())); - - List imageUrls = fileInformationService.getImageUrls( - IMAGE.getFileType() + FIX_ZONE.getFileDomain() + fix.getId()); - - return AdminDetailFixResponse.builder() - .id(fix.getId()) - .title(fix.getTitle()) - .createdAt(fix.getCreatedAt()) - .club(fix.getClub().getName()) - .location(fix.getClub().getLocation().getValue()) - .content(fix.getContent()) - .isCompleted(fix.isCompleted()) - .imageUrls(imageUrls).build(); - } - - public void update(Long fixId, UpdateFixRequest request) { - Fix fix = fixRepository.findById(fixId) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX.getText())); - - fix.update(request); - } - - public void updateIsCompleted(Long fixId, UpdateFiXCompletionRequest request) { - Fix fix = fixRepository.findById(fixId) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX.getText())); - - fix.updateIsCompleted(request.isCompleted()); - } - - public void delete(Long fixId) { - Fix fix = fixRepository.findById(fixId) - .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX.getText())); - fixRepository.delete(fix); - } + private final FixZoneRepository fixZoneRepository; + private final FileInformationService fileInformationService; + + @Transactional + public Long create(Club club, CreateFixZoneRequest request) { + FixZone createdFixZone = request.toEntity(club); + + return fixZoneRepository.save(createdFixZone).getId(); + } + + public GetDetailFixZoneResponse getFixZoneDetail(Long fixZoneId) { + FixZone fixZone = fixZoneRepository.findById(fixZoneId) + .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX.getText())); + + List imageUrls = fileInformationService.getImageUrls( + IMAGE.getFileType() + FIX_ZONE.getFileDomain() + fixZone.getId() + ); + + return GetDetailFixZoneResponse.of( + fixZone.getId(), + fixZone.getClub().getLocation().getValue(), + fixZone.getClub().getName(), + fixZone.getTitle(), + fixZone.getCreatedAt(), + fixZone.getContent(), + fixZone.isCompleted(), + imageUrls, + getCommentResponses(fixZone) + ); + } + + private List getCommentResponses(FixZone fixZone) { + List comments = fixZone.getFixZoneComments(); + + return comments.stream() + .map(comment -> { + List profileImageUrls = fileInformationService.getImageUrls( + IMAGE.getFileType() + CLUB_PROFILE.getFileDomain() + comment.getClub().getId() + ); + String profileImageUrl = profileImageUrls.isEmpty() ? null : profileImageUrls.get(0); + return GetFixZoneCommentResponse.of(comment, profileImageUrl); + }) + .toList(); + } + + public List getAll() { + return fixZoneRepository.findAll().stream() + .map(GetFixZoneResponse::of) + .toList(); + } + + @Transactional + public void update(Long fixZoneId, UpdateFixZoneRequest request) { + FixZone fixZone = getById(fixZoneId); + + fixZone.update( + request.getTitle(), + request.getContent() + ); + } + + @Transactional + public void updateToComplete(Long fixZoneId) { + FixZone fixZone = getById(fixZoneId); + + fixZone.updateToComplete(); + } + + @Transactional + public void delete(Long fixZoneId) { + FixZone fixZone = getById(fixZoneId); + fixZoneRepository.delete(fixZone); + } + + public List getMyFixZones(Long clubId) { + return fixZoneRepository.findAllByClubId(clubId) + .stream() + .map(GetFixZoneResponse::of) + .toList(); + } + + public FixZone getById(Long fixZoneId) { + return fixZoneRepository.findById(fixZoneId) + .orElseThrow(() -> new IllegalArgumentException(NO_SUCH_FIX.getText())); + } + } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/notice/entity/Notice.java b/src/main/java/ddingdong/ddingdongBE/domain/notice/entity/Notice.java index 31e49ded..13d69b6a 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/notice/entity/Notice.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/notice/entity/Notice.java @@ -3,6 +3,8 @@ import ddingdong.ddingdongBE.common.BaseEntity; import ddingdong.ddingdongBE.domain.notice.controller.dto.request.UpdateNoticeRequest; import ddingdong.ddingdongBE.domain.user.entity.User; +import java.time.LocalDateTime; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; @@ -10,14 +12,20 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update notice set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(name = "notice") public class Notice extends BaseEntity { @Id @@ -32,6 +40,9 @@ public class Notice extends BaseEntity { private String content; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder public Notice(User user, String title, String content) { this.user = user; diff --git a/src/main/java/ddingdong/ddingdongBE/domain/qrstamp/entity/StampHistory.java b/src/main/java/ddingdong/ddingdongBE/domain/qrstamp/entity/StampHistory.java index 5536e020..1bc98d8c 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/qrstamp/entity/StampHistory.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/qrstamp/entity/StampHistory.java @@ -16,14 +16,18 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.Where; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"studentName", "studentNumber"})) @TypeDef(name = "json", typeClass = JsonType.class) +@SQLDelete(sql = "update stamp_history set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(name = "stamp_history", uniqueConstraints = @UniqueConstraint(columnNames = {"studentName", "studentNumber"})) public class StampHistory extends BaseEntity { @Id @@ -48,8 +52,10 @@ public class StampHistory extends BaseEntity { private String certificationImageUrl; - @Builder + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder private StampHistory(Long id, String studentName, String department, String studentNumber, String telephone, LocalDateTime completedAt, String certificationImageUrl) { this.id = id; diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/api/AdminQuestionApi.java b/src/main/java/ddingdong/ddingdongBE/domain/question/api/AdminQuestionApi.java new file mode 100644 index 00000000..36285530 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/api/AdminQuestionApi.java @@ -0,0 +1,54 @@ +package ddingdong.ddingdongBE.domain.question.api; + + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.question.controller.dto.request.GenerateQuestionRequest; +import ddingdong.ddingdongBE.domain.question.controller.dto.request.ModifyQuestionRequest; +import ddingdong.ddingdongBE.domain.question.controller.dto.response.AdminQuestionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "FAQ - Admin", description = "FAQ Admin API") +@RequestMapping("/server/admin/questions") +public interface AdminQuestionApi { + + @Operation(summary = "μ–΄λ“œλ―Ό FAQ μ—…λ‘œλ“œ API") + @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + @ResponseStatus(HttpStatus.CREATED) + @SecurityRequirement(name = "AccessToken") + void generateQuestion( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @ModelAttribute GenerateQuestionRequest generateDocumentRequest); + + @Operation(summary = "μ–΄λ“œλ―Ό FAQ λͺ©λ‘ 쑰회 API") + @GetMapping + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + List getAllQuestions(); + + @Operation(summary = "μ–΄λ“œλ―Ό FAQ μˆ˜μ • API") + @PatchMapping(value = "/{questionId}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void modifyQuestion(@PathVariable Long questionId, + @ModelAttribute ModifyQuestionRequest modifyQuestionRequest); + + @Operation(summary = "μ–΄λ“œλ―Ό FAQ μ‚­μ œ API") + @DeleteMapping("/{questionId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + void deleteQuestion(@PathVariable Long questionId); +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/api/QuestionApi.java b/src/main/java/ddingdong/ddingdongBE/domain/question/api/QuestionApi.java new file mode 100644 index 00000000..63ec6a4f --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/api/QuestionApi.java @@ -0,0 +1,22 @@ +package ddingdong.ddingdongBE.domain.question.api; + + +import ddingdong.ddingdongBE.domain.question.controller.dto.response.QuestionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "FAQ", description = "FAQ API") +@RequestMapping("/server/questions") +public interface QuestionApi { + + @Operation(summary = "FAQ λͺ©λ‘ 쑰회 API") + @GetMapping + @ResponseStatus(HttpStatus.OK) + List getAllQuestions(); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/controller/AdminQuestionController.java b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/AdminQuestionController.java new file mode 100644 index 00000000..f413488c --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/AdminQuestionController.java @@ -0,0 +1,42 @@ +package ddingdong.ddingdongBE.domain.question.controller; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.question.api.AdminQuestionApi; +import ddingdong.ddingdongBE.domain.question.controller.dto.request.GenerateQuestionRequest; +import ddingdong.ddingdongBE.domain.question.controller.dto.request.ModifyQuestionRequest; +import ddingdong.ddingdongBE.domain.question.controller.dto.response.AdminQuestionResponse; +import ddingdong.ddingdongBE.domain.question.service.QuestionService; +import ddingdong.ddingdongBE.domain.user.entity.User; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class AdminQuestionController implements AdminQuestionApi { + + private final QuestionService questionService; + + @Override + public void generateQuestion(PrincipalDetails principalDetails, GenerateQuestionRequest generateDocumentRequest) { + User admin = principalDetails.getUser(); + questionService.create(generateDocumentRequest.toEntity(admin)); + } + + @Override + public List getAllQuestions() { + return questionService.getAll().stream() + .map(AdminQuestionResponse::from) + .toList(); + } + + @Override + public void modifyQuestion(Long questionId, ModifyQuestionRequest modifyQuestionRequest) { + questionService.update(questionId, modifyQuestionRequest.toEntity()); + } + + @Override + public void deleteQuestion(Long questionId) { + questionService.delete(questionId); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/controller/QuestionController.java b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/QuestionController.java new file mode 100644 index 00000000..5d712034 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/QuestionController.java @@ -0,0 +1,22 @@ +package ddingdong.ddingdongBE.domain.question.controller; + +import ddingdong.ddingdongBE.domain.question.api.QuestionApi; +import ddingdong.ddingdongBE.domain.question.controller.dto.response.QuestionResponse; +import ddingdong.ddingdongBE.domain.question.service.QuestionService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class QuestionController implements QuestionApi { + + private final QuestionService questionService; + + @Override + public List getAllQuestions() { + return questionService.getAll().stream() + .map(QuestionResponse::from) + .toList(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/request/GenerateQuestionRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/request/GenerateQuestionRequest.java new file mode 100644 index 00000000..e6804da4 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/request/GenerateQuestionRequest.java @@ -0,0 +1,26 @@ +package ddingdong.ddingdongBE.domain.question.controller.dto.request; + +import ddingdong.ddingdongBE.domain.question.entity.Question; +import ddingdong.ddingdongBE.domain.user.entity.User; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema( + name = "GenerateQuestionRequest", + description = "FAQ 질문 생성 μš”μ²­" +) +@Builder +public record GenerateQuestionRequest( + @Schema(description = "FAQ 질문", example = "질문") + String question, + @Schema(description = "FAQ λ‹΅λ³€", example = "λ‹΅λ³€") + String reply +) { + + public Question toEntity(User user) { + return Question.builder() + .user(user) + .question(this.question) + .reply(this.reply).build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/request/ModifyQuestionRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/request/ModifyQuestionRequest.java new file mode 100644 index 00000000..125f4bc2 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/request/ModifyQuestionRequest.java @@ -0,0 +1,25 @@ +package ddingdong.ddingdongBE.domain.question.controller.dto.request; + +import ddingdong.ddingdongBE.domain.question.entity.Question; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema( + name = "ModifyQuestionRequest", + description = "FAQ 질문 μˆ˜μ • μš”μ²­" +) +@Builder +public record ModifyQuestionRequest( + @Schema(description = "자료 제λͺ©", example = "제λͺ©") + String question, + @Schema(description = "자료 λ‚΄μš©", example = "λ‚΄μš©") + String reply +) { + + public Question toEntity() { + return Question.builder() + .question(this.question) + .reply(this.reply) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/response/AdminQuestionResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/response/AdminQuestionResponse.java new file mode 100644 index 00000000..9e00e9db --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/response/AdminQuestionResponse.java @@ -0,0 +1,35 @@ +package ddingdong.ddingdongBE.domain.question.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.question.entity.Question; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import lombok.Builder; + +@Schema( + name = "AdminQuestionResponse", + description = "μ–΄λ“œλ―Ό - FAQ 질문 λͺ©λ‘ 응닡" +) +@Builder +public record AdminQuestionResponse( + + @Schema(description = "질문 μ‹λ³„μž", example = "1") + Long id, + @Schema(description = "FAQ 질문", example = "질문") + String question, + @Schema(description = "FAQ λ‹΅λ³€", example = "λ‹΅λ³€") + String reply, + @Schema(description = "μž‘μ„±μΌ", example = "2024-01-01") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate createdAt +) { + + public static AdminQuestionResponse from(Question question) { + return AdminQuestionResponse.builder() + .id(question.getId()) + .question(question.getQuestion()) + .reply(question.getReply()) + .createdAt(question.getCreatedAt().toLocalDate()) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/response/QuestionResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/response/QuestionResponse.java new file mode 100644 index 00000000..367b52b9 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/controller/dto/response/QuestionResponse.java @@ -0,0 +1,31 @@ +package ddingdong.ddingdongBE.domain.question.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.question.entity.Question; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import lombok.Builder; + +@Schema( + name = "QuestionResponse", + description = "μœ μ € - FAQ 질문 λͺ©λ‘ 응닡" +) +@Builder +public record QuestionResponse( + + @Schema(description = "질문 μ‹λ³„μž", example = "1") + Long id, + @Schema(description = "FAQ 질문", example = "질문") + String question, + @Schema(description = "FAQ λ‹΅λ³€", example = "λ‹΅λ³€") + String reply +) { + + public static QuestionResponse from(Question question) { + return QuestionResponse.builder() + .id(question.getId()) + .question(question.getQuestion()) + .reply(question.getReply()) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/entity/Question.java b/src/main/java/ddingdong/ddingdongBE/domain/question/entity/Question.java new file mode 100644 index 00000000..605a1125 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/entity/Question.java @@ -0,0 +1,60 @@ +package ddingdong.ddingdongBE.domain.question.entity; + +import ddingdong.ddingdongBE.common.BaseEntity; +import ddingdong.ddingdongBE.domain.user.entity.User; +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Entity +@Getter +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update question set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(name = "question") +public class Question extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false) + private String question; + + @Column(nullable = false) + private String reply; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Builder + private Question(Long id, User user, String question, String reply, LocalDateTime createdAt) { + this.id = id; + this.user = user; + this.question = question; + this.reply = reply; + super.setCreatedAt(createdAt); + } + + public void updateQuestion(Question updatedDocument) { + this.question = updatedDocument.getQuestion(); + this.reply = updatedDocument.getReply(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/repository/QuestionRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/question/repository/QuestionRepository.java new file mode 100644 index 00000000..fa661459 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/repository/QuestionRepository.java @@ -0,0 +1,8 @@ +package ddingdong.ddingdongBE.domain.question.repository; + +import ddingdong.ddingdongBE.domain.question.entity.Question; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuestionRepository extends JpaRepository { + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/question/service/QuestionService.java b/src/main/java/ddingdong/ddingdongBE/domain/question/service/QuestionService.java new file mode 100644 index 00000000..df40dda7 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/question/service/QuestionService.java @@ -0,0 +1,45 @@ +package ddingdong.ddingdongBE.domain.question.service; + +import static ddingdong.ddingdongBE.common.exception.ErrorMessage.NO_SUCH_QUESTION; + +import ddingdong.ddingdongBE.domain.question.entity.Question; +import ddingdong.ddingdongBE.domain.question.repository.QuestionRepository; +import java.util.List; +import java.util.NoSuchElementException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class QuestionService { + + private final QuestionRepository questionRepository; + + public Long create(Question question) { + Question createdQuestion = questionRepository.save(question); + return createdQuestion.getId(); + } + + @Transactional(readOnly = true) + public List getAll() { + return questionRepository.findAll(); + } + + public Long update(Long questionId, Question updatedDocument) { + Question question = getQuestion(questionId); + question.updateQuestion(updatedDocument); + return question.getId(); + } + + public void delete(Long questionId) { + Question question = getQuestion(questionId); + questionRepository.delete(question); + } + + private Question getQuestion(Long questionId) { + return questionRepository.findById(questionId) + .orElseThrow(() -> new NoSuchElementException(NO_SUCH_QUESTION.getText())); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/api/AdminScoreHistoryApi.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/api/AdminScoreHistoryApi.java new file mode 100644 index 00000000..73ccdedf --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/api/AdminScoreHistoryApi.java @@ -0,0 +1,88 @@ +package ddingdong.ddingdongBE.domain.scorehistory.api; + +import ddingdong.ddingdongBE.common.exception.ErrorResponse; +import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.request.CreateScoreHistoryRequest; +import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response.ScoreHistoryFilterByClubResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "ScoreHistory - Admin", description = "ScoreHistory Admin API") +@RequestMapping("/server/admin/{clubId}/score") +public interface AdminScoreHistoryApi { + + @Operation(summary = "μ–΄λ“œλ―Ό 점수 등둝 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "점수 등둝 성곡"), + @ApiResponse(responseCode = "400", + description = "잘λͺ»λœ μš”μ²­", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ μ μˆ˜λ³€λ™λ‚΄μ—­ μΉ΄ν…Œκ³ λ¦¬", + value = """ + { + "status": 400, + "message": "μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ μ μˆ˜λ³€λ™λ‚΄μ—­ μΉ΄ν…Œκ³ λ¦¬μž…λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """), + @ExampleObject(name = "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 동아리", + value = """ + { + "status": 400, + "message": "μ‘΄μž¬ν•˜μ§€μ•ŠλŠ” λ™μ•„λ¦¬μž…λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """), + }) + ) + }) + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @SecurityRequirement(name = "AccessToken") + void register(@PathVariable Long clubId, @Valid @RequestBody CreateScoreHistoryRequest createScoreHistoryRequest); + + @Operation(summary = "μ–΄λ“œλ―Ό 동아리 점수 λ‚΄μ—­ λͺ©λ‘ 쑰회 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "점수 변동 λ‚΄μ—­ λͺ©λ‘ 쑰회 성곡", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ScoreHistoryFilterByClubResponse.class))), + @ApiResponse(responseCode = "400", + description = "잘λͺ»λœ μš”μ²­", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 동아리", + value = """ + { + "status": 400, + "message": "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ™μ•„λ¦¬μž…λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """ + ) + }) + ) + }) + @GetMapping + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + ScoreHistoryFilterByClubResponse findAllScoreHistories(@PathVariable Long clubId); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/api/ClubScoreHistoryApi.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/api/ClubScoreHistoryApi.java new file mode 100644 index 00000000..fb8c9c7f --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/api/ClubScoreHistoryApi.java @@ -0,0 +1,24 @@ +package ddingdong.ddingdongBE.domain.scorehistory.api; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response.ScoreHistoryFilterByClubResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "ScoreHistory - Club", description = "ScoreHistory Club API") +@RequestMapping("/server/club/my/score") +public interface ClubScoreHistoryApi { + + @Operation(summary = "동아리 λ‚΄ 점수 λ‚΄μ—­ λͺ©λ‘ 쑰회 API") + @GetMapping + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + ScoreHistoryFilterByClubResponse findMyScoreHistories(@AuthenticationPrincipal PrincipalDetails principalDetails); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/AdminScoreHistoryController.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/AdminScoreHistoryController.java index 8f72b939..3f479d39 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/AdminScoreHistoryController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/AdminScoreHistoryController.java @@ -1,38 +1,32 @@ package ddingdong.ddingdongBE.domain.scorehistory.controller; -import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.request.RegisterScoreRequest; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.service.ClubService; +import ddingdong.ddingdongBE.domain.scorehistory.api.AdminScoreHistoryApi; +import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.request.CreateScoreHistoryRequest; import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response.ScoreHistoryFilterByClubResponse; +import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response.ScoreHistoryFilterByClubResponse.ScoreHistoryResponse; import ddingdong.ddingdongBE.domain.scorehistory.service.ScoreHistoryService; - import java.util.List; import lombok.RequiredArgsConstructor; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor -@RequestMapping("/server/admin/{clubId}/score") -public class AdminScoreHistoryController { +public class AdminScoreHistoryController implements AdminScoreHistoryApi { + private final ClubService clubService; private final ScoreHistoryService scoreHistoryService; - @PostMapping - public void register( - @PathVariable Long clubId, - @RequestBody RegisterScoreRequest registerScoreRequest - ) { - scoreHistoryService.register(clubId, registerScoreRequest); + public void register(Long clubId, CreateScoreHistoryRequest createScoreHistoryRequest) { + scoreHistoryService.create(clubId, createScoreHistoryRequest); } - @GetMapping - public List getScoreHistories( - @PathVariable Long clubId - ) { - return scoreHistoryService.getScoreHistories(clubId); + public ScoreHistoryFilterByClubResponse findAllScoreHistories(Long clubId) { + Club club = clubService.getByClubId(clubId); + List scoreHistoryResponses = scoreHistoryService.findAllByClubId(clubId).stream() + .map(ScoreHistoryResponse::from) + .toList(); + return ScoreHistoryFilterByClubResponse.of(club, scoreHistoryResponses); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/ClubScoreHistoryController.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/ClubScoreHistoryController.java index 8f5f9762..ff6d2b65 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/ClubScoreHistoryController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/ClubScoreHistoryController.java @@ -1,28 +1,29 @@ package ddingdong.ddingdongBE.domain.scorehistory.controller; import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.service.ClubService; +import ddingdong.ddingdongBE.domain.scorehistory.api.ClubScoreHistoryApi; import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response.ScoreHistoryFilterByClubResponse; +import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response.ScoreHistoryFilterByClubResponse.ScoreHistoryResponse; import ddingdong.ddingdongBE.domain.scorehistory.service.ScoreHistoryService; import java.util.List; - import lombok.RequiredArgsConstructor; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor -@RequestMapping("/server/club/my/score") -public class ClubScoreHistoryController { +public class ClubScoreHistoryController implements ClubScoreHistoryApi { + private final ClubService clubService; private final ScoreHistoryService scoreHistoryService; - @GetMapping - public List getMyScoreHistories( - @AuthenticationPrincipal PrincipalDetails principalDetails - ) { - return scoreHistoryService.getMyScoreHistories(principalDetails.getUser().getId()); + public ScoreHistoryFilterByClubResponse findMyScoreHistories(PrincipalDetails principalDetails) { + Club club = clubService.getByUserId(principalDetails.getUser().getId()); + List scoreHistoryResponses = scoreHistoryService.findAllByUserId(club.getId()) + .stream() + .map(ScoreHistoryResponse::from) + .toList(); + return ScoreHistoryFilterByClubResponse.of(club, scoreHistoryResponses); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/request/CreateScoreHistoryRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/request/CreateScoreHistoryRequest.java new file mode 100644 index 00000000..6071ed73 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/request/CreateScoreHistoryRequest.java @@ -0,0 +1,40 @@ +package ddingdong.ddingdongBE.domain.scorehistory.controller.dto.request; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreCategory; +import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreHistory; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Builder; + +@Schema( + name = "CreateScoreHistoryRequest", + description = "μ–΄λ“œλ―Ό - 동아리 점수 변동 λ‚΄μ—­ 생성 " +) +@Builder +public record CreateScoreHistoryRequest( + @Schema(description = "μ μˆ˜λ³€λ™λ‚΄μ—­ μΉ΄ν…Œκ³ λ¦¬", + example = "ACTIVITY_REPORT", + allowableValues = {"CLEANING", "ACTIVITY_REPORT", "LEADER_CONFERENCE", "BUSINESS_PARTICIPATION", + "ADDITIONAL", "CARRYOVER_SCORE"} + ) + @NotNull(message = "μ μˆ˜λ³€λ™λ‚΄μ—­ μΉ΄ν…Œκ³ λ¦¬λŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + String scoreCategory, + + @Schema(description = "μ μˆ˜λ³€λ™λ‚΄μ—­ 원인", example = "1회차 ν™œλ™λ³΄κ³ μ„œ μž‘μ„±") + String reason, + + @Schema(description = "변동 점수", example = "10") + @NotNull(message = "변동 μ μˆ˜λŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + float amount +) { + + public ScoreHistory toEntity(Club club) { + return ScoreHistory.builder() + .club(club) + .amount(amount) + .scoreCategory(ScoreCategory.from(scoreCategory)) + .reason(reason) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/request/RegisterScoreRequest.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/request/RegisterScoreRequest.java deleted file mode 100644 index f1b94252..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/request/RegisterScoreRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package ddingdong.ddingdongBE.domain.scorehistory.controller.dto.request; - -import ddingdong.ddingdongBE.domain.club.entity.Club; -import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreCategory; -import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreHistory; -import lombok.Getter; - -@Getter -public class RegisterScoreRequest { - - private String scoreCategory; - - private String reason; - - private float amount; - - public ScoreHistory toEntity(Club club, float remainingScore) { - return ScoreHistory.builder() - .club(club) - .amount(amount) - .scoreCategory(ScoreCategory.of(scoreCategory)) - .reason(reason) - .remainingScore(remainingScore) - .build(); - } -} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/response/ScoreHistoryFilterByClubResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/response/ScoreHistoryFilterByClubResponse.java index 9938aed9..1f4d7485 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/response/ScoreHistoryFilterByClubResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/controller/dto/response/ScoreHistoryFilterByClubResponse.java @@ -1,39 +1,60 @@ package ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response; +import com.fasterxml.jackson.annotation.JsonFormat; +import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreHistory; - +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +import java.util.List; import lombok.Builder; -import lombok.Getter; - -@Getter -public class ScoreHistoryFilterByClubResponse { - private String scoreCategory; - - private String reason; - private float amount; +@Schema( + name = "ScoreHistoryFilterByClubResponse", + description = "μ–΄λ“œλ―Ό - 동아리 점수 변동 λ‚΄μ—­ λͺ©λ‘ 응닡" +) +@Builder +public record ScoreHistoryFilterByClubResponse( - private float remainingScore; + @Schema(description = "동아리 총 점수", example = "50") + float totalScore, + @ArraySchema(schema = @Schema(description = "μ μˆ˜λ‚΄μ—­ λͺ©λ‘", implementation = ScoreHistoryResponse.class)) + List scoreHistories +) { - private LocalDateTime createdAt; + public static ScoreHistoryFilterByClubResponse of(Club club, List scoreHistories) { + return ScoreHistoryFilterByClubResponse.builder() + .totalScore(club.getScore().getValue()) + .scoreHistories(scoreHistories) + .build(); + } + @Schema( + name = "ScoreHistoryResponse", + description = "점수 변동 λ‚΄μ—­ 응닡" + ) @Builder - public ScoreHistoryFilterByClubResponse(String scoreCategory, String reason, float amount, float remainingScore, LocalDateTime createdAt) { - this.scoreCategory = scoreCategory; - this.reason = reason; - this.amount = amount; - this.remainingScore = remainingScore; - this.createdAt = createdAt; - } + public record ScoreHistoryResponse( + + @Schema(description = "점수 λ‚΄μ—­ μΉ΄ν…Œκ³ λ¦¬", example = "ν™œλ™λ³΄κ³ μ„œ") + String scoreCategory, + @Schema(description = "점수 λ‚΄μ—­ 이유", example = "ν™œλ™λ³΄κ³ μ„œ μž‘μ„±") + String reason, + @Schema(description = "변동 점수", example = "10") + float amount, + @Schema(description = "μž‘μ„±μΌ", example = "2024-01-01") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime createdAt + ) { + + public static ScoreHistoryResponse from(ScoreHistory scoreHistory) { + return ScoreHistoryResponse.builder() + .scoreCategory(scoreHistory.getScoreCategory().getCategory()) + .reason(scoreHistory.getReason()) + .amount(scoreHistory.getAmount()) + .createdAt(scoreHistory.getCreatedAt()) + .build(); + } - public static ScoreHistoryFilterByClubResponse of(final ScoreHistory scoreHistory) { - return ScoreHistoryFilterByClubResponse.builder() - .scoreCategory(scoreHistory.getScoreCategory().getCategory()) - .reason(scoreHistory.getReason()) - .amount(scoreHistory.getAmount()) - .remainingScore(scoreHistory.getRemainingScore()) - .createdAt(scoreHistory.getCreatedAt()) - .build(); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/Score.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/Score.java index b7af860e..c97aa98b 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/Score.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/Score.java @@ -1,15 +1,19 @@ package ddingdong.ddingdongBE.domain.scorehistory.entity; +import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; + import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embeddable; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder public class Score { @Column(name = "score") @@ -36,9 +40,15 @@ public int hashCode() { return Objects.hash(getValue()); } - public static Score of(float value) { + public static Score from(float value) { + validateScore(value); return new Score(value); } + private static void validateScore(float value) { + if (value < 0.0F || value > 1000.0F) { + throw new IllegalArgumentException(INVALID_CLUB_SCORE_VALUE.getText()); + } + } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreCategory.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreCategory.java index 33306884..a0463b89 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreCategory.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreCategory.java @@ -2,6 +2,7 @@ import static ddingdong.ddingdongBE.common.exception.ErrorMessage.*; +import ddingdong.ddingdongBE.common.exception.InvalidatedMappingException.InvalidatedEnumValue; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -17,12 +18,11 @@ public enum ScoreCategory { private final String category; - public static ScoreCategory of(String category) { - for (ScoreCategory scoreCategory : ScoreCategory.values()) { - if (scoreCategory.category.equalsIgnoreCase(category)) { - return scoreCategory; - } + public static ScoreCategory from(String category) { + try { + return ScoreCategory.valueOf(category); + } catch (IllegalArgumentException e) { + throw new InvalidatedEnumValue(ILLEGAL_SCORE_CATEGORY.getText()); } - throw new IllegalArgumentException(ILLEGAL_SCORE_CATEGORY.getText()); } } \ No newline at end of file diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreHistory.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreHistory.java index d4e83823..ee66a932 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreHistory.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreHistory.java @@ -3,6 +3,8 @@ import ddingdong.ddingdongBE.common.BaseEntity; import ddingdong.ddingdongBE.domain.club.entity.Club; +import java.time.LocalDateTime; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -13,14 +15,20 @@ import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update score_history set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(name = "score_history") public class ScoreHistory extends BaseEntity { @Id @@ -38,14 +46,14 @@ public class ScoreHistory extends BaseEntity { private String reason; - private float remainingScore; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; @Builder - public ScoreHistory(Club club, float amount, ScoreCategory scoreCategory, String reason, float remainingScore) { + public ScoreHistory(Club club, float amount, ScoreCategory scoreCategory, String reason) { this.club = club; this.amount = amount; this.scoreCategory = scoreCategory; this.reason = reason; - this.remainingScore = remainingScore; } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/service/ScoreHistoryService.java b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/service/ScoreHistoryService.java index a433af93..7f4b0e1a 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/service/ScoreHistoryService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/scorehistory/service/ScoreHistoryService.java @@ -2,13 +2,11 @@ import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.club.service.ClubService; -import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.request.RegisterScoreRequest; -import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.response.ScoreHistoryFilterByClubResponse; +import ddingdong.ddingdongBE.domain.scorehistory.controller.dto.request.CreateScoreHistoryRequest; +import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreHistory; import ddingdong.ddingdongBE.domain.scorehistory.repository.ScoreHistoryRepository; - import java.util.List; import lombok.RequiredArgsConstructor; - import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,34 +18,26 @@ public class ScoreHistoryService { private final ScoreHistoryRepository scoreHistoryRepository; private final ClubService clubService; - public void register(final Long clubId, RegisterScoreRequest registerScoreRequest) { - Club club = clubService.findClubByClubId(clubId); - - float score = roundToThirdPoint(registerScoreRequest.getAmount()); + public void create(final Long clubId, CreateScoreHistoryRequest createScoreHistoryRequest) { + Club club = clubService.getByClubId(clubId); - float remainingScore = clubService.editClubScore(clubId, score); - - scoreHistoryRepository.save(registerScoreRequest.toEntity(club, remainingScore)); + float score = roundToThirdPoint(createScoreHistoryRequest.amount()); + clubService.updateClubScore(clubId, score); + scoreHistoryRepository.save(createScoreHistoryRequest.toEntity(club)); } @Transactional(readOnly = true) - public List getScoreHistories(final Long clubId) { - - return scoreHistoryRepository.findByClubId(clubId).stream() - .map(ScoreHistoryFilterByClubResponse::of) - .toList(); + public List findAllByClubId(final Long clubId) { + return scoreHistoryRepository.findByClubId(clubId); } @Transactional(readOnly = true) - public List getMyScoreHistories(final Long userId) { - Club club = clubService.findClubByUserId(userId); - - return scoreHistoryRepository.findByClubId(club.getId()).stream() - .map(ScoreHistoryFilterByClubResponse::of) - .toList(); + public List findAllByUserId(final Long userId) { + Club club = clubService.getByUserId(userId); + return scoreHistoryRepository.findByClubId(club.getId()); } - private static float roundToThirdPoint(float value) { + private float roundToThirdPoint(float value) { return Math.round(value * 1000.0) / 1000.0F; } -} \ No newline at end of file +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/user/entity/User.java b/src/main/java/ddingdong/ddingdongBE/domain/user/entity/User.java index 9d567e09..d8c4713f 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/user/entity/User.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/user/entity/User.java @@ -1,6 +1,7 @@ package ddingdong.ddingdongBE.domain.user.entity; import ddingdong.ddingdongBE.common.BaseEntity; +import java.time.LocalDateTime; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -9,14 +10,20 @@ import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "update users set deleted_at = CURRENT_TIMESTAMP where id=?") +@Where(clause = "deleted_at IS NULL") +@Table(name = "users") public class User extends BaseEntity { @Id @@ -33,8 +40,12 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private Role role; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder - public User(String userId, String password, String name, Role role) { + public User(Long id, String userId, String password, String name, Role role) { + this.id = id; this.userId = userId; this.password = password; this.name = name; diff --git a/src/main/java/ddingdong/ddingdongBE/file/api/S3FileAPi.java b/src/main/java/ddingdong/ddingdongBE/file/api/S3FileAPi.java new file mode 100644 index 00000000..48624f27 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/api/S3FileAPi.java @@ -0,0 +1,58 @@ +package ddingdong.ddingdongBE.file.api; + +import ddingdong.ddingdongBE.common.exception.ErrorResponse; +import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Tag(name = "S3File", description = "AWS S3 File API") +@RequestMapping("/server/file") +public interface S3FileAPi { + + @Operation(summary = "AWS S3 presignedUrl λ°œκΈ‰ API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "presignedUrl λ°œκΈ‰ 성곡"), + @ApiResponse(responseCode = "400", + description = "AWS 였λ₯˜(μ„œλ²„ 였λ₯˜)", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "AWS μ„œλΉ„μŠ€ 였λ₯˜(μ„œλ²„ 였λ₯˜)", + value = """ + { + "status": 500, + "message": "AWS μ„œλΉ„μŠ€ 였λ₯˜λ‘œ 인해 Presigned URL 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """ + ), + @ExampleObject(name = "AWS ν΄λΌμ΄μ–ΈνŠΈ 였λ₯˜(μ„œλ²„ 였λ₯˜)", + value = """ + { + "status": 500, + "message": "AWS ν΄λΌμ΄μ–ΈνŠΈ 였λ₯˜λ‘œ 인해 Presigned URL 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.", + "timestamp": "2024-08-22T00:08:46.990585" + } + """ + ) + }) + ) + }) + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + @GetMapping("/upload-url") + UploadUrlResponse getUploadUrl(@RequestParam("fileName") String fileName); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/controller/S3FileController.java b/src/main/java/ddingdong/ddingdongBE/file/controller/S3FileController.java new file mode 100644 index 00000000..f46c2386 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/controller/S3FileController.java @@ -0,0 +1,19 @@ +package ddingdong.ddingdongBE.file.controller; + +import ddingdong.ddingdongBE.file.api.S3FileAPi; +import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; +import ddingdong.ddingdongBE.file.service.S3FileService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class S3FileController implements S3FileAPi { + + private final S3FileService s3FileService; + + @Override + public UploadUrlResponse getUploadUrl(String fileName) { + return s3FileService.generatePreSignedUrl(fileName); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/controller/dto/response/UploadUrlResponse.java b/src/main/java/ddingdong/ddingdongBE/file/controller/dto/response/UploadUrlResponse.java new file mode 100644 index 00000000..ed8c1695 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/controller/dto/response/UploadUrlResponse.java @@ -0,0 +1,26 @@ +package ddingdong.ddingdongBE.file.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema( + name = "UploadUrlResponse", + description = "파일 - μ—…λ‘œλ“œ url 쑰회 응닡" +) +@Builder +public record UploadUrlResponse( + + @Schema(description = "presignedUrl", example = "https://test-bucket.s3.amazonaws.com/test/jpg/image.jpg") + String uploadUrl, + @Schema(description = "μ—…λ‘œλ“œ 파일 이름(UUID)", example = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d") + String uploadFileName +) { + + public static UploadUrlResponse of(String uploadUrl, String uploadFileName) { + return UploadUrlResponse.builder() + .uploadUrl(uploadUrl) + .uploadFileName(uploadFileName) + .build(); + } + +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/dto/ExcelClubMemberDto.java b/src/main/java/ddingdong/ddingdongBE/file/dto/ExcelClubMemberDto.java new file mode 100644 index 00000000..8e86c8a1 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/dto/ExcelClubMemberDto.java @@ -0,0 +1,90 @@ +package ddingdong.ddingdongBE.file.dto; + +import ddingdong.ddingdongBE.common.exception.InvalidatedMappingException.InvalidatedEnumValue; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.entity.ClubMember; +import ddingdong.ddingdongBE.domain.club.entity.Position; +import java.util.Arrays; +import java.util.Iterator; +import lombok.Builder; +import lombok.Getter; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; + +@Getter +public class ExcelClubMemberDto { + + private static final DataFormatter formatter = new DataFormatter(); + + private Long id; + + private String name; + + private String studentNumber; + + private String phoneNumber; + + private String position; + + private String department; + + @Builder + private ExcelClubMemberDto(Long id, String name, String studentNumber, String phoneNumber, String position, + String department) { + this.id = id; + this.name = name; + this.studentNumber = studentNumber; + this.phoneNumber = phoneNumber; + this.position = position; + this.department = department; + } + + + public ClubMember toEntity(Club club) { + return ClubMember.builder() + .id(id) + .club(club) + .name(name) + .studentNumber(studentNumber) + .phoneNumber(phoneNumber) + .position(Position.valueOf(position)) + .department(department).build(); + } + + public static ExcelClubMemberDto fromExcelRow(Row row) { + ExcelClubMemberDto clubMemberDto = ExcelClubMemberDto.builder().build(); + Iterator cellIterator = row.cellIterator(); + while (cellIterator.hasNext()) { + Cell cell = cellIterator.next(); + if (cell.getCellType() == CellType.STRING && cell.getStringCellValue() != null) { + clubMemberDto.setValueByCell(cell.getStringCellValue(), cell.getColumnIndex()); + } else if (cell.getCellType() == CellType.NUMERIC && cell.getNumericCellValue() != 0) { + String stringCellValue = formatter.formatCellValue(cell); + clubMemberDto.setValueByCell(stringCellValue, cell.getColumnIndex()); + } + } + return clubMemberDto; + } + + private void setValueByCell(String stringCellValue, int columnIndex) { + switch (columnIndex) { + case 0 -> this.id = Long.valueOf(stringCellValue); + case 1 -> this.name = stringCellValue; + case 2 -> this.studentNumber = stringCellValue; + case 3 -> this.phoneNumber = stringCellValue; + case 4 -> { + validatePositionValue(stringCellValue); + this.position = stringCellValue; + } + case 5 -> this.department = stringCellValue; + } + } + + private void validatePositionValue(String stringCellValue) { + if (Arrays.stream(Position.values()).noneMatch(position -> position.name().equals(stringCellValue))) { + throw new InvalidatedEnumValue("λ™μ•„λ¦¬μ›μ˜ 역할은 LEADER, EXECUTIVE, MEMBER 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€."); + } + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/dto/FileResponse.java b/src/main/java/ddingdong/ddingdongBE/file/dto/FileResponse.java index 71197ac8..aee0ae99 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/dto/FileResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/file/dto/FileResponse.java @@ -1,13 +1,24 @@ package ddingdong.ddingdongBE.file.dto; import ddingdong.ddingdongBE.domain.fileinformation.entity.FileInformation; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; import lombok.Getter; +@Schema( + name = "FileResponse", + description = "파일 정보 응닡" +) @Getter public class FileResponse { + + @Schema(description = "파일 이름", example = "a.pdf") private String name; + + @Schema(description = "파일 링크", example = "https://a.b") private String fileUrl; + @Builder public FileResponse(String name, String fileUrl) { this.name = name; this.fileUrl = fileUrl; diff --git a/src/main/java/ddingdong/ddingdongBE/file/dto/UploadFileDto.java b/src/main/java/ddingdong/ddingdongBE/file/dto/UploadFileDto.java index 958c186b..b993b675 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/dto/UploadFileDto.java +++ b/src/main/java/ddingdong/ddingdongBE/file/dto/UploadFileDto.java @@ -13,8 +13,6 @@ public class UploadFileDto { private String storedFileName; - private String key; - @Builder public UploadFileDto(String uploadFileName, String storedFileName) { this.uploadFileName = uploadFileName; diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/ExcelFileService.java b/src/main/java/ddingdong/ddingdongBE/file/service/ExcelFileService.java new file mode 100644 index 00000000..03a6f4bd --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/service/ExcelFileService.java @@ -0,0 +1,142 @@ +package ddingdong.ddingdongBE.file.service; + +import ddingdong.ddingdongBE.common.exception.ParsingExcelFileException.ExcelIO; +import ddingdong.ddingdongBE.common.exception.ParsingExcelFileException.NonExcelFile; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.entity.ClubMember; +import ddingdong.ddingdongBE.file.dto.ExcelClubMemberDto; +import java.awt.Color; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class ExcelFileService { + + private static final String CLUB_MEMBER_EXCEL_MANUAL_IMAGE_PATH = "src/main/resources/static/club_member_excel_menual.png"; + + public byte[] generateClubMemberListFile(List clubMembers) { + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("동아리원 λͺ…단"); + sheet.setZoom(125); + createHeaderRow(workbook, sheet); + createDataRow(clubMembers, sheet); + addManualImage(workbook, sheet); + + for (int i = 0; i < 13; i++) { + sheet.autoSizeColumn(i); + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + workbook.write(outputStream); + return outputStream.toByteArray(); + } catch (Exception e) { + throw new RuntimeException("Failed to generate Excel file", e); + } + } + + public List extractClubMembersInformation(Club club, MultipartFile file) { + isExcelFile(file); + List requestedClubMembersDto = parsingClubMemberListFile(file); + return requestedClubMembersDto.stream() + .map(clubMemberDto -> clubMemberDto.toEntity(club)) + .toList(); + } + + private void isExcelFile(MultipartFile file) { + String fileName = file.getOriginalFilename(); + if (fileName != null && !(fileName.endsWith(".xls") || fileName.endsWith(".xlsx"))) { + throw new NonExcelFile(); + } + } + + private List parsingClubMemberListFile(MultipartFile clubMemberListFile) { + List requestedClubMembersDto = new ArrayList<>(); + try { + Workbook workbook = new XSSFWorkbook(clubMemberListFile.getInputStream()); + Sheet sheet = workbook.getSheetAt(0); + for (Row row : sheet) { + if (row.getRowNum() != 0 && row.getCell(row.getFirstCellNum()).getCellType() != CellType.BLANK) { + requestedClubMembersDto.add(ExcelClubMemberDto.fromExcelRow(row)); + } + } + } catch (IOException | NotOfficeXmlFileException e) { + throw new ExcelIO(); + } + return requestedClubMembersDto; + } + + + private void createHeaderRow(Workbook workbook, Sheet sheet) { + XSSFCellStyle headerCellStyle = (XSSFCellStyle) workbook.createCellStyle(); + XSSFColor myColor = new XSSFColor(new Color(177, 207, 149), null); + headerCellStyle.setFillForegroundColor(myColor); + headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerCellStyle.setFont(headerFont); + + Row headerRow = sheet.createRow(0); + String[] headers = {"μ‹λ³„μž(μˆ˜μ •X)", "이름", "ν•™λ²ˆ", "μ—°λ½μ²˜", "비ꡐ(μž„μ›μ§„) - μ˜μ–΄λ§Œ", "ν•™κ³Ό(λΆ€)"}; + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerCellStyle); + } + + } + + private void createDataRow(List clubMembers, Sheet sheet) { + int rowNum = 1; + for (ClubMember clubMember : clubMembers) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(clubMember.getId()); + row.createCell(1).setCellValue(clubMember.getName()); + row.createCell(2).setCellValue(clubMember.getStudentNumber()); + row.createCell(3).setCellValue(clubMember.getPhoneNumber()); + row.createCell(4).setCellValue(clubMember.getPosition().name()); + row.createCell(5).setCellValue(clubMember.getDepartment()); + } + } + + private void addManualImage(Workbook workbook, Sheet sheet) throws IOException { + InputStream inputStream = new FileInputStream(CLUB_MEMBER_EXCEL_MANUAL_IMAGE_PATH); + byte[] bytes = IOUtils.toByteArray(inputStream); + int pictureIdx = workbook.addPicture(bytes, Workbook.PICTURE_TYPE_PNG); + inputStream.close(); + + CreationHelper helper = workbook.getCreationHelper(); + Drawing drawing = sheet.createDrawingPatriarch(); + ClientAnchor anchor = helper.createClientAnchor(); + + anchor.setCol1(8); + anchor.setRow1(0); + anchor.setCol2(anchor.getCol1() + 7); + anchor.setRow2(26); + + drawing.createPicture(anchor, pictureIdx); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/S3FileService.java b/src/main/java/ddingdong/ddingdongBE/file/service/S3FileService.java new file mode 100644 index 00000000..f54554ea --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/service/S3FileService.java @@ -0,0 +1,85 @@ +package ddingdong.ddingdongBE.file.service; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.github.f4b6a3.uuid.UuidCreator; +import ddingdong.ddingdongBE.common.exception.AwsException.AwsClient; +import ddingdong.ddingdongBE.common.exception.AwsException.AwsService; +import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; +import java.net.URL; +import java.util.Date; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class S3FileService { + + @Value("${spring.s3.bucket}") + private String bucketName; + + @Value("${spring.config.activate.on-profile}") + private String serverProfile; + + private final AmazonS3Client amazonS3Client; + + public UploadUrlResponse generatePreSignedUrl(String fileName) { + UUID uploadFileName = UuidCreator.getTimeOrderedEpoch(); + String s3FilePath = createFilePath(fileName, uploadFileName); + + Date expiration = setExpirationTime(); + try { + GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, + s3FilePath) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration); + + URL uploadUrl = amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest); + return UploadUrlResponse.of(uploadUrl.toString(), uploadFileName.toString()); + } catch (AmazonServiceException e) { + log.warn("AWS Service Error : {}", e.getMessage()); + throw new AwsService(); + } catch (AmazonClientException e) { + log.warn("AWS Client Error : {}", e.getMessage()); + throw new AwsClient(); + } + + } + + public String getUploadedFileUrl(String fileName, String uploadFileName) { + String region = amazonS3Client.getRegionName(); + String fileExtension = extractFileExtension(fileName); + + return String.format("https://%s.s3.%s.amazonaws.com/%s/%s/%s", + bucketName, + region, + serverProfile, + fileExtension, + uploadFileName); + } + + private Date setExpirationTime() { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 5; + expiration.setTime(expTimeMillis); + return expiration; + } + + private String createFilePath(String fileName, UUID uploadFileName) { + String fileExtension = extractFileExtension(fileName); + return String.format("%s/%s/%s", serverProfile, fileExtension, uploadFileName.toString()); + } + + private String extractFileExtension(String fileName) { + return fileName.substring(fileName.lastIndexOf('.') + 1); + } + +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c705c13f..8f38da68 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -5,14 +5,18 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DEV_DB_URL} + username: ${DEV_DB_USERNAME} + password: ${DEV_DB_PASSWORD} jpa: database: mysql - database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + database-platform: org.hibernate.dialect.MySQL8InnoDBDialect hibernate: ddl-auto: none properties: - hibernate: + hibernate.format_sql: true + dialect: org.hibernate.dialect.MySQL8InnoDBDialect defer-datasource-initialization: false sql: @@ -25,8 +29,3 @@ jwt: issuer: "ddingdong" secret: ${JWT_SECRET} expiration: 36000 - -sentry: - dsn: ${SENTRY_DSN_KEY} - enable-tracing: true - environment: dev diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 6268863e..041345b5 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -3,25 +3,25 @@ spring: activate: on-profile: local + flyway: + enabled: false + datasource: - url: jdbc:h2:tcp://localhost/~/projects/ddingdong/ddingdong;NON_KEYWORDS=user; - driver-class-name: org.h2.Driver - username: sa - password: + url: jdbc:mysql://localhost:3307/ddingdong_local_db + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 1234 jpa: hibernate: ddl-auto: create - show-sql: true properties: hibernate: format_sql: true + dialect: org.hibernate.dialect.MySQL8Dialect + show-sql: true defer-datasource-initialization: true sql: init: mode: always - - h2: - console: - enabled: true diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..24ae6186 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,30 @@ +spring: + config: + activate: + on-profile: prod + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + jpa: + database: mysql + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + ddl-auto: none + properties: + hibernate: + defer-datasource-initialization: false + + sql: + init: + mode: never + +jwt: + header: "Authorization" + prefix: "Bearer" + issuer: "ddingdong" + secret: ${JWT_SECRET} + expiration: 36000 diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index bfbdfd4f..b272b78e 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -3,21 +3,49 @@ spring: activate: on-profile: test - datasource: - url: jdbc:h2:tcp://localhost/~/projects/ddingdong/ddingdong - driver-class-name: org.h2.Driver - username: sa - password: - + flyway: + enabled: false + jpa: hibernate: ddl-auto: create - show-sql: true properties: hibernate: + show-sql: true format_sql: true auto_quote_keyword: true + dialect: org.hibernate.dialect.MySQL8Dialect sql: init: mode: never + + s3: + bucket: "test" + access-key: "test" + secret-key: "test" + + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + +cloud: + aws: + region: + static: "ap-northeast-2" + stack: + auto: false + + +jwt: + header: "Authorization" + prefix: "Bearer" + issuer: "ddingdong" + secret: "test" + expiration: 3600 + +logging: + level: + org.hibernate.SQL: debug + org.hibernate.type.descriptor.sql: trace diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2b5ba187..bd10f7c1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,12 +1,12 @@ spring: - profiles: - default: dev - datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ${DB_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} + + flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 0 + locations: classpath:db/migration jpa: hibernate: @@ -36,11 +36,6 @@ cloud: stack: auto: false -sentry: - dsn: ${SENTRY_KEY} - enable-tracing: true - environment: dev - swagger: server: url: ${SERVER_URL:http://localhost:8080} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index b6c23b17..bf014957 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,4 +1,4 @@ -insert into user(name, password, role, userid) +insert into users(name, password, role, userid) values ('ddingdong', '$2a$12$9BIi3IGc79rU3Xgbnxq/X.T37Hlfrf/lSc2/g0HLeM1g7HmFXE8v.', 'ADMIN', 'ddingdong11'), ('cow', '$2a$12$9BIi3IGc79rU3Xgbnxq/X.T37Hlfrf/lSc2/g0HLeM1g7HmFXE8v.', 'CLUB', 'cow11'); diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/db/migration/V2__Add_softdelete_column.sql b/src/main/resources/db/migration/V2__Add_softdelete_column.sql new file mode 100644 index 00000000..b23cd4ab --- /dev/null +++ b/src/main/resources/db/migration/V2__Add_softdelete_column.sql @@ -0,0 +1,14 @@ +ALTER TABLE activity_report ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE activity_report_term_info ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE banner ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE club ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE club_member ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE document ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE file_information ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE fix_zone ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE fix_zone_comment ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE notice ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE question ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE score_history ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE stamp_history ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP; diff --git a/src/main/resources/static/club_member_excel_menual.png b/src/main/resources/static/club_member_excel_menual.png new file mode 100644 index 00000000..0173f516 Binary files /dev/null and b/src/main/resources/static/club_member_excel_menual.png differ diff --git a/src/test/java/ddingdong/ddingdongBE/common/config/TestConfig.java b/src/test/java/ddingdong/ddingdongBE/common/config/TestConfig.java new file mode 100644 index 00000000..85380072 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/config/TestConfig.java @@ -0,0 +1,10 @@ +package ddingdong.ddingdongBE.common.config; + +import ddingdong.ddingdongBE.common.support.DataInitializer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Import; + +@TestConfiguration +@Import(DataInitializer.class) +public class TestConfig { +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/DataInitializer.java b/src/test/java/ddingdong/ddingdongBE/common/support/DataInitializer.java new file mode 100644 index 00000000..2ecc1fcd --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/DataInitializer.java @@ -0,0 +1,49 @@ +package ddingdong.ddingdongBE.common.support; + +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.util.ArrayList; +import java.util.List; +import javax.persistence.EntityManager; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Profile("test") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Component +public class DataInitializer { + + private static final String OFF_FOREIGN_CONSTRAINTS = "SET foreign_key_checks = false"; + private static final String ON_FOREIGN_CONSTRAINTS = "SET foreign_key_checks = true"; + private static final String TRUNCATE_SQL_FORMAT = "TRUNCATE %s"; + + private static final List truncationDMLs = new ArrayList<>(); + + @PersistenceContext + private EntityManager em; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteAll() { + if (truncationDMLs.isEmpty()) { + init(); + } + + em.createNativeQuery(OFF_FOREIGN_CONSTRAINTS).executeUpdate(); + truncationDMLs.stream() + .map(em::createNativeQuery) + .forEach(Query::executeUpdate); + em.createNativeQuery(ON_FOREIGN_CONSTRAINTS).executeUpdate(); + } + + private void init() { + final List tableNames = em.createNativeQuery("SHOW TABLES ").getResultList(); + + tableNames.stream() + .map(tableName -> String.format(TRUNCATE_SQL_FORMAT, tableName)) + .forEach(truncationDMLs::add); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/DataJpaTestSupport.java b/src/test/java/ddingdong/ddingdongBE/common/support/DataJpaTestSupport.java new file mode 100644 index 00000000..a32f1f22 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/DataJpaTestSupport.java @@ -0,0 +1,13 @@ +package ddingdong.ddingdongBE.common.support; + +import ddingdong.ddingdongBE.common.config.TestConfig; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public abstract class DataJpaTestSupport extends TestContainerSupport { +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/FixtureMonkeyFactory.java b/src/test/java/ddingdong/ddingdongBE/common/support/FixtureMonkeyFactory.java new file mode 100644 index 00000000..35fc0cbc --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/FixtureMonkeyFactory.java @@ -0,0 +1,14 @@ +package ddingdong.ddingdongBE.common.support; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import com.navercorp.fixturemonkey.api.introspector.BuilderArbitraryIntrospector; + +public class FixtureMonkeyFactory { + + public static FixtureMonkey getBuilderIntrospectorMonkey() { + return FixtureMonkey.builder() + .objectIntrospector(BuilderArbitraryIntrospector.INSTANCE) + .build(); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/TestContainerSupport.java b/src/test/java/ddingdong/ddingdongBE/common/support/TestContainerSupport.java new file mode 100644 index 00000000..5db5d9c2 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/TestContainerSupport.java @@ -0,0 +1,52 @@ +package ddingdong.ddingdongBE.common.support; + + +import static lombok.AccessLevel.PROTECTED; + +import ddingdong.ddingdongBE.common.config.TestConfig; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.MySQLContainer; + + +@NoArgsConstructor(access = PROTECTED) +@ActiveProfiles("test") +@Import(TestConfig.class) +public abstract class TestContainerSupport { + + private static final String MYSQL_IMAGE = "mysql:8"; + private static final int MYSQL_PORT = 3306; + private static final JdbcDatabaseContainer MYSQL; + + @Autowired + private DataInitializer dataInitializer; + + // 싱글톀 + static { + MYSQL = new MySQLContainer<>(MYSQL_IMAGE) + .withExposedPorts(MYSQL_PORT) + .withReuse(true); + + MYSQL.start(); + } + + // λ™μ μœΌλ‘œ DB 속성 ν• λ‹Ή + @DynamicPropertySource + public static void setUp(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.driver-class-name", MYSQL::getDriverClassName); + registry.add("spring.datasource.url", MYSQL::getJdbcUrl); + registry.add("spring.datasource.username", MYSQL::getUsername); + registry.add("spring.datasource.password", MYSQL::getPassword); + } + + @BeforeEach + void delete() { + dataInitializer.deleteAll(); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/WebApiIntegrationTestSupport.java b/src/test/java/ddingdong/ddingdongBE/common/support/WebApiIntegrationTestSupport.java new file mode 100644 index 00000000..372cd292 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/WebApiIntegrationTestSupport.java @@ -0,0 +1,24 @@ +package ddingdong.ddingdongBE.common.support; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +public abstract class WebApiIntegrationTestSupport extends TestContainerSupport { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + protected String toJson(Object object) throws JsonProcessingException { + return objectMapper.writeValueAsString(object); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/WebApiUnitTestSupport.java b/src/test/java/ddingdong/ddingdongBE/common/support/WebApiUnitTestSupport.java new file mode 100644 index 00000000..00c70708 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/WebApiUnitTestSupport.java @@ -0,0 +1,70 @@ +package ddingdong.ddingdongBE.common.support; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ddingdong.ddingdongBE.domain.club.service.ClubService; +import ddingdong.ddingdongBE.domain.documents.controller.AdminDocumentController; +import ddingdong.ddingdongBE.domain.documents.controller.DocumentController; +import ddingdong.ddingdongBE.domain.documents.service.DocumentService; +import ddingdong.ddingdongBE.domain.fileinformation.service.FileInformationService; +import ddingdong.ddingdongBE.domain.question.controller.AdminQuestionController; +import ddingdong.ddingdongBE.domain.question.controller.QuestionController; +import ddingdong.ddingdongBE.domain.question.service.QuestionService; +import ddingdong.ddingdongBE.domain.scorehistory.controller.AdminScoreHistoryController; +import ddingdong.ddingdongBE.domain.scorehistory.controller.ClubScoreHistoryController; +import ddingdong.ddingdongBE.domain.scorehistory.service.ScoreHistoryService; +import ddingdong.ddingdongBE.file.service.FileService; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@ActiveProfiles("test") +@WebMvcTest(controllers = { + AdminDocumentController.class, + DocumentController.class, + AdminQuestionController.class, + QuestionController.class, + AdminScoreHistoryController.class, + ClubScoreHistoryController.class +}) +public abstract class WebApiUnitTestSupport { + + @Autowired + private WebApplicationContext context; + @Autowired + protected MockMvc mockMvc; + @MockBean + protected DocumentService documentService; + @MockBean + protected FileService fileService; + @MockBean + protected FileInformationService fileInformationService; + @MockBean + protected QuestionService questionService; + @MockBean + protected ClubService clubService; + @MockBean + protected ScoreHistoryService scoreHistoryService; + + @Autowired + protected ObjectMapper objectMapper; + + protected String toJson(Object object) throws JsonProcessingException { + return objectMapper.writeValueAsString(object); + } + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/WithMockAuthenticatedUser.java b/src/test/java/ddingdong/ddingdongBE/common/support/WithMockAuthenticatedUser.java new file mode 100644 index 00000000..244ac6d4 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/WithMockAuthenticatedUser.java @@ -0,0 +1,26 @@ +package ddingdong.ddingdongBE.common.support; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@WithSecurityContext(factory = WithMockAuthenticatedUserSecurityContextFactory.class) +public @interface WithMockAuthenticatedUser { + + long id() default 1L; + + String userId() default "user"; + + String role() default "USER"; + + String password() default "password"; + +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/WithMockAuthenticatedUserSecurityContextFactory.java b/src/test/java/ddingdong/ddingdongBE/common/support/WithMockAuthenticatedUserSecurityContextFactory.java new file mode 100644 index 00000000..2fb64836 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/support/WithMockAuthenticatedUserSecurityContextFactory.java @@ -0,0 +1,29 @@ +package ddingdong.ddingdongBE.common.support; + +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.user.entity.Role; +import ddingdong.ddingdongBE.domain.user.entity.User; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithMockAuthenticatedUserSecurityContextFactory implements + WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithMockAuthenticatedUser withMockPrincipalDetails) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + User user = User.builder() + .id(withMockPrincipalDetails.id()) + .userId(withMockPrincipalDetails.userId()) + .password(withMockPrincipalDetails.password()) + .role(Role.valueOf(withMockPrincipalDetails.role())).build(); + + PrincipalDetails principalDetails = new PrincipalDetails(user); + context.setAuthentication(new TestingAuthenticationToken(principalDetails, principalDetails.getPassword(), + String.valueOf(principalDetails.getAuthorities()))); + return context; + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReportTest.java b/src/test/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReportTest.java new file mode 100644 index 00000000..7b86d8c4 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/activityreport/domain/ActivityReportTest.java @@ -0,0 +1,40 @@ +package ddingdong.ddingdongBE.domain.activityreport.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import ddingdong.ddingdongBE.common.support.DataJpaTestSupport; +import ddingdong.ddingdongBE.domain.activityreport.repository.ActivityReportRepository; +import javax.persistence.EntityManager; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class ActivityReportTest extends DataJpaTestSupport { + + @Autowired + private ActivityReportRepository activityReportRepository; + + @Autowired + private EntityManager entityManager; + + @DisplayName("ν™œλ™λ³΄κ³ μ„œ μ‚­μ œ μ‹œ soft delete μ μš©ν•œλ‹€.") + @Test + void soft_delete() { + // given + ActivityReport activityReport = ActivityReport.builder() + .content("λ‚΄μš©μž…λ‹ˆλ‹€.") + .build(); + activityReportRepository.save(activityReport); + // when + activityReportRepository.delete(activityReport); + entityManager.flush(); + // then + ActivityReport findActivityReport = (ActivityReport) entityManager.createNativeQuery( + "select * from activity_report where content = :content limit 1", ActivityReport.class) + .setParameter("content", activityReport.getContent()) + .getSingleResult(); + Assertions.assertThat(findActivityReport).isNotNull(); + Assertions.assertThat(findActivityReport.getDeletedAt()).isNotNull(); + } +} \ No newline at end of file diff --git a/src/test/java/ddingdong/ddingdongBE/domain/club/entity/LocationTest.java b/src/test/java/ddingdong/ddingdongBE/domain/club/entity/LocationTest.java index 10e681a0..75ae85d9 100644 --- a/src/test/java/ddingdong/ddingdongBE/domain/club/entity/LocationTest.java +++ b/src/test/java/ddingdong/ddingdongBE/domain/club/entity/LocationTest.java @@ -15,7 +15,7 @@ class LocationTest { void createLocation(String givenValue) { //given //when - Location location = Location.of(givenValue); + Location location = Location.from(givenValue); //then assertThat(location.getValue()).isEqualTo(givenValue); @@ -27,7 +27,7 @@ void createLocation(String givenValue) { void createLocationWithIllegalRegex(String givenValue) { //given //when //then - assertThatThrownBy(() -> Location.of(givenValue)) + assertThatThrownBy(() -> Location.from(givenValue)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 동아리 μœ„μΉ˜ μ–‘μ‹μž…λ‹ˆλ‹€."); } diff --git a/src/test/java/ddingdong/ddingdongBE/domain/club/service/FacadeClubMemberServiceTest.java b/src/test/java/ddingdong/ddingdongBE/domain/club/service/FacadeClubMemberServiceTest.java new file mode 100644 index 00000000..c1740512 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/club/service/FacadeClubMemberServiceTest.java @@ -0,0 +1,134 @@ +package ddingdong.ddingdongBE.domain.club.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import ddingdong.ddingdongBE.common.support.FixtureMonkeyFactory; +import ddingdong.ddingdongBE.common.support.TestContainerSupport; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.entity.ClubMember; +import ddingdong.ddingdongBE.domain.club.entity.Position; +import ddingdong.ddingdongBE.domain.club.repository.ClubMemberRepository; +import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; +import ddingdong.ddingdongBE.domain.club.service.dto.UpdateClubMemberCommand; +import ddingdong.ddingdongBE.domain.user.entity.User; +import ddingdong.ddingdongBE.domain.user.repository.UserRepository; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +@SpringBootTest +class FacadeClubMemberServiceTest extends TestContainerSupport { + + @Autowired + private FacadeClubMemberService facadeClubMemberService; + @Autowired + private UserRepository userRepository; + @Autowired + private ClubRepository clubRepository; + @Autowired + private ClubMemberRepository clubMemberRepository; + @Autowired + private ClubMemberService clubMemberService; + + private final FixtureMonkey fixtureMonkey = FixtureMonkeyFactory.getBuilderIntrospectorMonkey(); + + @DisplayName("μ—‘μ…€ νŒŒμΌμ„ 톡해 동아리원 λͺ…단을 μˆ˜μ •ν•œλ‹€.") + @Test + void updateClubList() throws IOException { + //given + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Members"); + Row header = sheet.createRow(0); + header.createCell(0).setCellValue("id"); + header.createCell(1).setCellValue("이름"); + header.createCell(2).setCellValue("ν•™λ²ˆ"); + header.createCell(3).setCellValue("μ—°λ½μ²˜"); + header.createCell(4).setCellValue("비ꡐ(μž„μ›μ§„) - μ˜μ–΄λ§Œ"); + header.createCell(5).setCellValue("ν•™κ³Ό(λΆ€)"); + + Row row1 = sheet.createRow(1); + row1.createCell(0).setCellValue(1); + row1.createCell(1).setCellValue("5uhwann"); + row1.createCell(2).setCellValue("60001234"); + row1.createCell(3).setCellValue("010-1234-5678"); + row1.createCell(4).setCellValue("LEADER"); + row1.createCell(5).setCellValue("μœ΅ν•©μ†Œν”„νŠΈμ›¨μ–΄ν•™λΆ€"); + + Row row2 = sheet.createRow(2); + row2.createCell(0).setCellValue(6); + row2.createCell(1).setCellValue("5uhwann"); + row2.createCell(2).setCellValue(60001234); + row2.createCell(3).setCellValue("010-1234-5678"); + row2.createCell(4).setCellValue("LEADER"); + row2.createCell(5).setCellValue("μœ΅ν•©μ†Œν”„νŠΈμ›¨μ–΄ν•™λΆ€"); + workbook.write(out); + workbook.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + MultipartFile validExcelFile = new MockMultipartFile( + "file", + "valid_excel.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + in + ); + + User savedUser = userRepository.save(fixtureMonkey.giveMeOne(User.class)); + Club savedClub = clubRepository.save(fixtureMonkey.giveMeBuilder(Club.class).set("user", savedUser).sample()); + List clubMembers = fixtureMonkey.giveMeBuilder(ClubMember.class) + .set("club", savedClub) + .sampleList(5); + clubMemberRepository.saveAll(clubMembers); + + //when + facadeClubMemberService.updateMemberList(savedUser.getId(), validExcelFile); + + //then + List updatedClubMemberList = clubMemberRepository.findAll(); + boolean has3To6Id = updatedClubMemberList.stream() + .anyMatch(cm -> cm.getId() >= 3 && cm.getId() <= 5); + assertThat(updatedClubMemberList.size()).isEqualTo(2); + assertThat(has3To6Id).isFalse(); + } + + @DisplayName("동아리원 정보λ₯Ό μˆ˜μ •ν•œλ‹€.") + @Test + void update() { + //given + User savedUser = userRepository.save(fixtureMonkey.giveMeOne(User.class)); + Club savedClub = clubRepository.save(fixtureMonkey.giveMeBuilder(Club.class).set("user", savedUser).sample()); + ClubMember savedClubMember = clubMemberRepository.save( + fixtureMonkey.giveMeBuilder(ClubMember.class).set("club", savedClub).sample()); + + UpdateClubMemberCommand updateClubMemberCommand = UpdateClubMemberCommand.builder() + .name("test") + .phoneNumber("010-1234-5678") + .studentNumber("60001234") + .position(Position.LEADER) + .department("test").build(); + + //when + facadeClubMemberService.update(savedClubMember.getId(), updateClubMemberCommand); + + //then + ClubMember updatedClubMember = clubMemberService.getById(savedClubMember.getId()); + assertThat(updatedClubMember.getName()).isEqualTo("test"); + assertThat(updatedClubMember.getPhoneNumber()).isEqualTo("010-1234-5678"); + assertThat(updatedClubMember.getStudentNumber()).isEqualTo("60001234"); + assertThat(updatedClubMember.getPosition()).isEqualTo(Position.LEADER); + assertThat(updatedClubMember.getDepartment()).isEqualTo("test"); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/documents/controller/AdminDocumentControllerUnitTest.java b/src/test/java/ddingdong/ddingdongBE/domain/documents/controller/AdminDocumentControllerUnitTest.java new file mode 100644 index 00000000..0b230bc8 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/documents/controller/AdminDocumentControllerUnitTest.java @@ -0,0 +1,152 @@ +package ddingdong.ddingdongBE.domain.documents.controller; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ddingdong.ddingdongBE.domain.documents.controller.dto.request.GenerateDocumentRequest; +import ddingdong.ddingdongBE.domain.documents.controller.dto.request.ModifyDocumentRequest; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.file.dto.FileResponse; +import ddingdong.ddingdongBE.common.support.WebApiUnitTestSupport; +import ddingdong.ddingdongBE.common.support.WithMockAuthenticatedUser; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +public class AdminDocumentControllerUnitTest extends WebApiUnitTestSupport { + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("document 자료 생성 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void generateDocument() throws Exception { + // given + GenerateDocumentRequest request = GenerateDocumentRequest.builder() + .title("testTitle") + .content("testContent").build(); + MockMultipartFile file = new MockMultipartFile("uploadFiles", "test.txt", "text/plain", + "test content".getBytes()); + when(documentService.create(any())) + .thenReturn(1L); + + // when // then + mockMvc.perform(multipart("/server/admin/documents") + .file(file) + .param("title", request.title()) + .param("content", request.content()) + .contentType(MediaType.MULTIPART_FORM_DATA) + .with(csrf())) + .andDo(print()) + .andExpect(status().isCreated()); + + verify(fileService).uploadDownloadableFile(anyLong(), anyList(), any(), any()); + } + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("documents 쑰회 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void getAllDocumentsDocuments() throws Exception { + //given + List foundDocuments = List.of( + Document.builder().id(1L).title("A").createdAt(LocalDateTime.now()).build(), + Document.builder().id(2L).title("B").createdAt(LocalDateTime.now()).build()); + when(documentService.getAll()).thenReturn(foundDocuments); + + //when //then + mockMvc.perform(get("/server/admin/documents") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(foundDocuments.size()))) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].title").value("A")) + .andExpect(jsonPath("$[1].id").value(2L)) + .andExpect(jsonPath("$[1].title").value("B")); + } + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("documents μƒμ„Έμ‘°νšŒ μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void getDocument() throws Exception { + //given + Document document = Document.builder() + .title("title") + .content("content") + .createdAt(LocalDateTime.now()).build(); + when(documentService.getById(1L)).thenReturn(document); + + List fileResponses = List.of(FileResponse.builder().name("fileA").fileUrl("fileAUrl").build(), + FileResponse.builder().name("fileB").fileUrl("fileBUrl").build()); + when(fileInformationService.getFileUrls(any())).thenReturn(fileResponses); + + //when //then + mockMvc.perform(get("/server/admin/documents/{documentId}", 1L) + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("title")) + .andExpect(jsonPath("$.content").value("content")) + .andExpect(jsonPath("$.fileUrls", hasSize(fileResponses.size()))) + .andExpect(jsonPath("$.fileUrls[0].name").value("fileA")) + .andExpect(jsonPath("$.fileUrls[0].fileUrl").value("fileAUrl")); + } + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("document 자료 μˆ˜μ • μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void modify() throws Exception { + // given + ModifyDocumentRequest modifyRequest = ModifyDocumentRequest.builder() + .title("testTitle") + .content("testContent").build(); + MockMultipartFile file = new MockMultipartFile("uploadFilessymotion-prefix)", "test.txt", "text/plain", + "test content".getBytes()); + when(documentService.update(1L, modifyRequest.toEntity())).thenReturn(1L); + + // when // then + mockMvc.perform(multipart("/server/admin/documents/{documentId}", 1L) + .file(file) + .param("title", modifyRequest.title()) + .param("content", modifyRequest.content()) + .contentType(MediaType.MULTIPART_FORM_DATA) + .with(csrf()) + .with(request -> { + request.setMethod("PATCH"); + return request; + })) + .andDo(print()) + .andExpect(status().isNoContent()); + + verify(fileService).deleteFile(anyLong(), any(), any()); + verify(fileService).uploadDownloadableFile(anyLong(), any(), any(), any()); + } + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("documents μ‚­μ œ μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void deleteDocument() throws Exception { + //given + + //when //then + mockMvc.perform(delete("/server/admin/documents/{documentId}", 1L) + .with(csrf())) + .andDo(print()) + .andExpect(status().isNoContent()); + + verify(documentService).delete(1L); + verify(fileService).deleteFile(anyLong(), any(), any()); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/documents/controller/DocumentControllerUnitTest.java b/src/test/java/ddingdong/ddingdongBE/domain/documents/controller/DocumentControllerUnitTest.java new file mode 100644 index 00000000..acb9df82 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/documents/controller/DocumentControllerUnitTest.java @@ -0,0 +1,72 @@ +package ddingdong.ddingdongBE.domain.documents.controller; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.file.dto.FileResponse; +import ddingdong.ddingdongBE.common.support.WebApiUnitTestSupport; +import ddingdong.ddingdongBE.common.support.WithMockAuthenticatedUser; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DocumentControllerUnitTest extends WebApiUnitTestSupport { + + + @WithMockAuthenticatedUser + @DisplayName("documents 쑰회 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void getAllDocuments() throws Exception { + //given + List foundDocuments = List.of( + Document.builder().id(1L).title("A").createdAt(LocalDateTime.now()).build(), + Document.builder().id(2L).title("B").createdAt(LocalDateTime.now()).build()); + when(documentService.getAll()).thenReturn(foundDocuments); + + //when //then + mockMvc.perform(get("/server/documents") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(foundDocuments.size()))) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].title").value("A")) + .andExpect(jsonPath("$[1].id").value(2L)) + .andExpect(jsonPath("$[1].title").value("B")); + } + + @WithMockAuthenticatedUser + @DisplayName("documents μƒμ„Έμ‘°νšŒ μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void getDocument() throws Exception { + //given + Document document = Document.builder() + .title("title") + .content("content") + .createdAt(LocalDateTime.now()).build(); + when(documentService.getById(1L)).thenReturn(document); + + List fileResponses = List.of(FileResponse.builder().name("fileA").fileUrl("fileAUrl").build(), + FileResponse.builder().name("fileB").fileUrl("fileBUrl").build()); + when(fileInformationService.getFileUrls(any())).thenReturn(fileResponses); + + //when //then + mockMvc.perform(get("/server/documents/{documentId}", 1L) + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("title")) + .andExpect(jsonPath("$.content").value("content")) + .andExpect(jsonPath("$.fileUrls", hasSize(fileResponses.size()))) + .andExpect(jsonPath("$.fileUrls[0].name").value("fileA")) + .andExpect(jsonPath("$.fileUrls[0].fileUrl").value("fileAUrl")); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/documents/service/DocumentServiceTest.java b/src/test/java/ddingdong/ddingdongBE/domain/documents/service/DocumentServiceTest.java new file mode 100644 index 00000000..c646554d --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/documents/service/DocumentServiceTest.java @@ -0,0 +1,42 @@ +package ddingdong.ddingdongBE.domain.documents.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import ddingdong.ddingdongBE.common.support.TestContainerSupport; +import ddingdong.ddingdongBE.domain.documents.entity.Document; +import ddingdong.ddingdongBE.domain.documents.repository.DocumentRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DocumentServiceTest extends TestContainerSupport { + + @Autowired + private DocumentRepository documentRepository; + + @Autowired + private DocumentService documentService; + + + @DisplayName("document(자료)λ₯Ό μƒμ„±ν•œλ‹€.") + @Test + void create() { + //given + Document document = Document.builder() + .title("test") + .content("test") + .build(); + + //when + Long createdDocumentId = documentService.create(document); + + //then + Optional foundDocument = documentRepository.findById(createdDocumentId); + assertThat(foundDocument.isPresent()).isTrue(); + assertThat(foundDocument.get().getId()).isEqualTo(createdDocumentId); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/question/controller/AdminQuestionControllerUnitTest.java b/src/test/java/ddingdong/ddingdongBE/domain/question/controller/AdminQuestionControllerUnitTest.java new file mode 100644 index 00000000..3dfa611b --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/question/controller/AdminQuestionControllerUnitTest.java @@ -0,0 +1,115 @@ +package ddingdong.ddingdongBE.domain.question.controller; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ddingdong.ddingdongBE.domain.question.controller.dto.request.GenerateQuestionRequest; +import ddingdong.ddingdongBE.domain.question.controller.dto.request.ModifyQuestionRequest; +import ddingdong.ddingdongBE.domain.question.entity.Question; +import ddingdong.ddingdongBE.common.support.WebApiUnitTestSupport; +import ddingdong.ddingdongBE.common.support.WithMockAuthenticatedUser; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +class AdminQuestionControllerUnitTest extends WebApiUnitTestSupport { + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("question 생성 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void generateQuestion() throws Exception { + // given + GenerateQuestionRequest request = GenerateQuestionRequest.builder() + .question("testQuestion") + .reply("testReply").build(); + + // when // then + mockMvc.perform(post("/server/admin/questions") + .param("title", request.question()) + .param("content", request.reply()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .with(csrf())) + .andDo(print()) + .andExpect(status().isCreated()); + + verify(questionService).create(any()); + } + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("questions 쑰회 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void getAllDocumentsDocuments() throws Exception { + //given + LocalDateTime questionACreatedAt = LocalDateTime.now(); + LocalDateTime questionBCreatedAt = LocalDateTime.now(); + List foundQuestions = List.of( + Question.builder().id(1L).question("A").reply("A").createdAt(questionACreatedAt).build(), + Question.builder().id(2L).question("B").reply("B").createdAt(questionBCreatedAt).build()); + when(questionService.getAll()).thenReturn(foundQuestions); + + //when //then + mockMvc.perform(get("/server/admin/questions") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(foundQuestions.size()))) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].question").value("A")) + .andExpect(jsonPath("$[0].reply").value("A")) + .andExpect(jsonPath("$[0].createdAt").value(questionACreatedAt.toString().split("T")[0])) + .andExpect(jsonPath("$[1].id").value(2L)) + .andExpect(jsonPath("$[1].question").value("B")) + .andExpect(jsonPath("$[1].reply").value("B")) + .andExpect(jsonPath("$[1].createdAt").value(questionBCreatedAt.toString().split("T")[0])); + } + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("question 자료 μˆ˜μ • μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void modifyQuestion() throws Exception { + // given + ModifyQuestionRequest modifyRequest = ModifyQuestionRequest.builder() + .question("testQuestion") + .reply("testReply").build(); + + // when // then + mockMvc.perform(patch("/server/admin/questions/{questionId}", 1L) + .param("question", modifyRequest.question()) + .param("reply", modifyRequest.reply()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .with(csrf())) + .andDo(print()) + .andExpect(status().isNoContent()); + + verify(questionService).update(anyLong(), any()); + } + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("question μ‚­μ œ μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void deleteQuestion() throws Exception { + //given + + //when //then + mockMvc.perform(delete("/server/admin/questions/{questionId}", 1L) + .with(csrf())) + .andDo(print()) + .andExpect(status().isNoContent()); + + verify(questionService).delete(1L); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/question/controller/QuestionControllerUnitTest.java b/src/test/java/ddingdong/ddingdongBE/domain/question/controller/QuestionControllerUnitTest.java new file mode 100644 index 00000000..12fd38b7 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/question/controller/QuestionControllerUnitTest.java @@ -0,0 +1,45 @@ +package ddingdong.ddingdongBE.domain.question.controller; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ddingdong.ddingdongBE.domain.question.entity.Question; +import ddingdong.ddingdongBE.common.support.WebApiUnitTestSupport; +import ddingdong.ddingdongBE.common.support.WithMockAuthenticatedUser; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class QuestionControllerUnitTest extends WebApiUnitTestSupport { + + @WithMockAuthenticatedUser + @DisplayName("questions 쑰회 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void getAllDocumentsDocuments() throws Exception { + //given + List foundQuestions = List.of( + Question.builder().id(1L).question("A").reply("A").createdAt(LocalDateTime.now()).build(), + Question.builder().id(2L).question("B").reply("B").createdAt(LocalDateTime.now()).build()); + when(questionService.getAll()).thenReturn(foundQuestions); + + //when //then + mockMvc.perform(get("/server/questions") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(foundQuestions.size()))) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].question").value("A")) + .andExpect(jsonPath("$[0].reply").value("A")) + .andExpect(jsonPath("$[1].id").value(2L)) + .andExpect(jsonPath("$[1].question").value("B")) + .andExpect(jsonPath("$[1].reply").value("B")); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/question/service/QuestionServiceTest.java b/src/test/java/ddingdong/ddingdongBE/domain/question/service/QuestionServiceTest.java new file mode 100644 index 00000000..2b52924b --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/question/service/QuestionServiceTest.java @@ -0,0 +1,42 @@ +package ddingdong.ddingdongBE.domain.question.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import ddingdong.ddingdongBE.common.support.TestContainerSupport; +import ddingdong.ddingdongBE.domain.question.entity.Question; +import ddingdong.ddingdongBE.domain.question.repository.QuestionRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class QuestionServiceTest extends TestContainerSupport { + + @Autowired + private QuestionService questionService; + + @Autowired + private QuestionRepository questionRepository; + + + @DisplayName("document(자료)λ₯Ό μƒμ„±ν•œλ‹€.") + @Test + void create() { + //given + Question document = Question.builder() + .question("test") + .reply("test") + .build(); + + //when + Long createdQuestionId = questionService.create(document); + + //then + Optional foundDocument = questionRepository.findById(createdQuestionId); + assertThat(foundDocument.isPresent()).isTrue(); + assertThat(foundDocument.get().getId()).isEqualTo(createdQuestionId); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/controller/AdminScoreHistoryControllerUnitTest.java b/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/controller/AdminScoreHistoryControllerUnitTest.java new file mode 100644 index 00000000..c14ae701 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/controller/AdminScoreHistoryControllerUnitTest.java @@ -0,0 +1,57 @@ +package ddingdong.ddingdongBE.domain.scorehistory.controller; + +import static ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreCategory.ACTIVITY_REPORT; +import static ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreCategory.CARRYOVER_SCORE; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; +import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreHistory; +import ddingdong.ddingdongBE.common.support.WebApiUnitTestSupport; +import ddingdong.ddingdongBE.common.support.WithMockAuthenticatedUser; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AdminScoreHistoryControllerUnitTest extends WebApiUnitTestSupport { + + @WithMockAuthenticatedUser(role = "ADMIN") + @DisplayName("동아리 점수 λ‚΄μ—­ 쑰회 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void findAllScoreHistories() throws Exception { + //given + Club club = Club.builder() + .id(1L) + .score(Score.from(55)).build(); + List scoreHistories = List.of(ScoreHistory.builder() + .club(club) + .scoreCategory(CARRYOVER_SCORE) + .amount(5) + .reason("reasonA").build(), + ScoreHistory.builder() + .club(club) + .scoreCategory(ACTIVITY_REPORT) + .amount(5) + .reason("reasonB").build()); + when(clubService.getByClubId(anyLong())).thenReturn(club); + when(scoreHistoryService.findAllByClubId(club.getId())).thenReturn(scoreHistories); + + //when //then + mockMvc.perform(get("/server/admin/{clubId}/score", 1L) + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalScore").value(55)) + .andExpect(jsonPath("$.scoreHistories", hasSize(scoreHistories.size()))) + .andExpect(jsonPath("$.scoreHistories[0].scoreCategory").value(CARRYOVER_SCORE.getCategory())) + .andExpect(jsonPath("$.scoreHistories[0].reason").value("reasonA")) + .andExpect(jsonPath("$.scoreHistories[0].amount").value(5)); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/controller/ClubScoreHistoryControllerUnitTest.java b/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/controller/ClubScoreHistoryControllerUnitTest.java new file mode 100644 index 00000000..0842131f --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/controller/ClubScoreHistoryControllerUnitTest.java @@ -0,0 +1,58 @@ +package ddingdong.ddingdongBE.domain.scorehistory.controller; + +import static ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreCategory.ACTIVITY_REPORT; +import static ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreCategory.CARRYOVER_SCORE; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; +import ddingdong.ddingdongBE.domain.scorehistory.entity.ScoreHistory; +import ddingdong.ddingdongBE.common.support.WebApiUnitTestSupport; +import ddingdong.ddingdongBE.common.support.WithMockAuthenticatedUser; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ClubScoreHistoryControllerUnitTest extends WebApiUnitTestSupport { + + @WithMockAuthenticatedUser(role = "CLUB") + @DisplayName("동아리- λ‚΄ 점수 λ‚΄μ—­ 쑰회 μš”μ²­μ„ μˆ˜ν–‰ν•œλ‹€.") + @Test + void getScoreHistories() throws Exception { + //given + Club club = Club.builder() + .id(1L) + .score(Score.from(55)).build(); + List scoreHistories = List.of(ScoreHistory.builder() + .club(club) + .scoreCategory(CARRYOVER_SCORE) + .amount(5) + .reason("reasonA").build(), + ScoreHistory.builder() + .club(club) + .scoreCategory(ACTIVITY_REPORT) + .amount(5) + .reason("reasonB").build()); + when(clubService.getByUserId(anyLong())).thenReturn(club); + when(scoreHistoryService.findAllByUserId(club.getId())).thenReturn(scoreHistories); + + //when //then + mockMvc.perform(get("/server/club/my/score") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalScore").value(55)) + .andExpect(jsonPath("$.scoreHistories", hasSize(scoreHistories.size()))) + .andExpect(jsonPath("$.scoreHistories[0].scoreCategory").value(CARRYOVER_SCORE.getCategory())) + .andExpect(jsonPath("$.scoreHistories[0].reason").value("reasonA")) + .andExpect(jsonPath("$.scoreHistories[0].amount").value(5)); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreCategoryTest.java b/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreCategoryTest.java new file mode 100644 index 00000000..0392ad5b --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/scorehistory/entity/ScoreCategoryTest.java @@ -0,0 +1,22 @@ +package ddingdong.ddingdongBE.domain.scorehistory.entity; + +import static ddingdong.ddingdongBE.common.exception.InvalidatedMappingException.InvalidatedEnumValue; + +import ddingdong.ddingdongBE.common.exception.ErrorMessage; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class ScoreCategoryTest { + + @Test + void from() { + //given + String scoreCategoryName = "CLEAN"; + + //when //then + Assertions.assertThatThrownBy(() -> ScoreCategory.from(scoreCategoryName)) + .isInstanceOf(InvalidatedEnumValue.class) + .hasMessage(ErrorMessage.ILLEGAL_SCORE_CATEGORY.getText()); + } + +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/user/repository/UserRepositoryTest.java b/src/test/java/ddingdong/ddingdongBE/domain/user/repository/UserRepositoryTest.java index 97c6b4bd..19ed64eb 100644 --- a/src/test/java/ddingdong/ddingdongBE/domain/user/repository/UserRepositoryTest.java +++ b/src/test/java/ddingdong/ddingdongBE/domain/user/repository/UserRepositoryTest.java @@ -3,6 +3,7 @@ import static ddingdong.ddingdongBE.domain.user.entity.Role.*; import static org.assertj.core.api.Assertions.*; +import ddingdong.ddingdongBE.common.support.DataJpaTestSupport; import ddingdong.ddingdongBE.domain.user.entity.User; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -11,9 +12,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.ActiveProfiles; -@ActiveProfiles("test") -@DataJpaTest -class UserRepositoryTest { +class UserRepositoryTest extends DataJpaTestSupport { @Autowired private UserRepository userRepository; diff --git a/src/test/java/ddingdong/ddingdongBE/file/service/ExcelFileServiceTest.java b/src/test/java/ddingdong/ddingdongBE/file/service/ExcelFileServiceTest.java new file mode 100644 index 00000000..dac19509 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/file/service/ExcelFileServiceTest.java @@ -0,0 +1,222 @@ +package ddingdong.ddingdongBE.file.service; + +import static ddingdong.ddingdongBE.common.exception.ParsingExcelFileException.ExcelIO; +import static ddingdong.ddingdongBE.domain.club.entity.Position.LEADER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.groups.Tuple.tuple; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import ddingdong.ddingdongBE.common.exception.InvalidatedMappingException.InvalidatedEnumValue; +import ddingdong.ddingdongBE.common.exception.ParsingExcelFileException; +import ddingdong.ddingdongBE.common.exception.ParsingExcelFileException.NonExcelFile; +import ddingdong.ddingdongBE.common.support.FixtureMonkeyFactory; +import ddingdong.ddingdongBE.common.support.TestContainerSupport; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.entity.ClubMember; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +@SpringBootTest +class ExcelFileServiceTest extends TestContainerSupport { + + @Autowired + private ExcelFileService excelFileService; + + private final FixtureMonkey fixtureMonkey = FixtureMonkeyFactory.getBuilderIntrospectorMonkey(); + + + @Test + @DisplayName("μ—‘μ…€ νŒŒμΌμ„ μ •μƒμ μœΌλ‘œ νŒŒμ‹±ν•˜λŠ” 경우") + void extractClubMembersInformationWithValidatedExcelFile() throws IOException { + //given + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Members"); + Row header = sheet.createRow(0); + header.createCell(0).setCellValue("id"); + header.createCell(1).setCellValue("이름"); + header.createCell(2).setCellValue("ν•™λ²ˆ"); + header.createCell(3).setCellValue("μ—°λ½μ²˜"); + header.createCell(4).setCellValue("비ꡐ(μž„μ›μ§„) - μ˜μ–΄λ§Œ"); + header.createCell(5).setCellValue("ν•™κ³Ό(λΆ€)"); + + Row row1 = sheet.createRow(1); + row1.createCell(0).setCellValue(1); + row1.createCell(1).setCellValue("5uhwann"); + row1.createCell(2).setCellValue("60001234"); + row1.createCell(3).setCellValue("010-1234-5678"); + row1.createCell(4).setCellValue("LEADER"); + row1.createCell(5).setCellValue("μœ΅ν•©μ†Œν”„νŠΈμ›¨μ–΄ν•™λΆ€"); + + Row row2 = sheet.createRow(2); + row2.createCell(0).setCellValue(2); + row2.createCell(1).setCellValue("5uhwann"); + row2.createCell(2).setCellValue(60001234); + row2.createCell(3).setCellValue("010-1234-5678"); + row2.createCell(4).setCellValue("LEADER"); + row2.createCell(5).setCellValue("μœ΅ν•©μ†Œν”„νŠΈμ›¨μ–΄ν•™λΆ€"); + workbook.write(out); + workbook.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + MultipartFile validExcelFile = new MockMultipartFile( + "file", + "valid_excel.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + in + ); + + Club club = Club.builder().id(1L).build(); + + // when + List clubMembers = excelFileService.extractClubMembersInformation(club, validExcelFile); + + // then + assertThat(clubMembers).hasSize(2) + .extracting("club", "name", "studentNumber", "phoneNumber", "position", "department") + .contains(tuple(club, "5uhwann", "60001234", "010-1234-5678", LEADER, "μœ΅ν•©μ†Œν”„νŠΈμ›¨μ–΄ν•™λΆ€")); + } + + @DisplayName("μ—‘μ…€νŒŒμΌμ΄ μ•„λ‹Œ 경우 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + @Test + void extractClubMembersInformationWithNonExcelFile() { + //given + Club club = Club.builder().id(1L).build(); + MultipartFile nonExcelFile = new MockMultipartFile( + "file", + "not_excel.txt", + "text/plain", + "some data".getBytes() + ); + + //when //then + assertThatThrownBy(() -> excelFileService.extractClubMembersInformation(club, nonExcelFile)) + .isInstanceOf(NonExcelFile.class) + .hasMessage(ParsingExcelFileException.NON_EXCEL_FILE_ERROR_MESSAGE); + } + + + @Test + @DisplayName("μ—‘μ…€ 파일 νŒŒμ‹± 쀑 IO μ˜ˆμ™Έ λ°œμƒ") + void extractClubMembersInformationWithIOException() throws IOException { + // given + byte[] invalidContent = new byte[]{0x00, 0x01, 0x02}; // 잘λͺ»λœ 데이터 + MultipartFile invalidExcelFile = new MockMultipartFile( + "file", + "invalid_excel.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + new ByteArrayInputStream(invalidContent) + ); + Club club = Club.builder().id(1L).build(); + + // when + assertThatThrownBy(() -> excelFileService.extractClubMembersInformation(club, invalidExcelFile)) + .isInstanceOf(ExcelIO.class) + .hasMessage(ParsingExcelFileException.EXCEL_IO_ERROR_MESSAGE); + } + + @DisplayName("동아리원 λͺ…단 μ—‘μ…€ νŒŒμΌμ—μ„œ μ˜¬λ°”λ₯Έ 동아리원 μ—­ν• (LEADER, EXECUTION, MEMBER)이 아닐 경우 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + @Test + void extractClubMembersInformationWithNonValidatedStringCellValue() throws IOException { + //given + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Members"); + Row header = sheet.createRow(0); + header.createCell(0).setCellValue("id"); + header.createCell(1).setCellValue("이름"); + header.createCell(2).setCellValue("ν•™λ²ˆ"); + header.createCell(3).setCellValue("μ—°λ½μ²˜"); + header.createCell(4).setCellValue("비ꡐ(μž„μ›μ§„) - μ˜μ–΄λ§Œ"); + header.createCell(5).setCellValue("ν•™κ³Ό(λΆ€)"); + + Row row1 = sheet.createRow(1); + row1.createCell(0).setCellValue(1); + row1.createCell(1).setCellValue("5uhwann"); + row1.createCell(2).setCellValue("60001234"); + row1.createCell(3).setCellValue("010-1234-5678"); + row1.createCell(4).setCellValue("member"); + row1.createCell(5).setCellValue("μœ΅ν•©μ†Œν”„νŠΈμ›¨μ–΄ν•™λΆ€"); + + workbook.write(out); + workbook.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + MultipartFile nonValidExcelFile = new MockMultipartFile( + "file", + "valid_excel.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + in + ); + + Club club = Club.builder().id(1L).build(); + + //when //then + assertThatThrownBy(() -> excelFileService.extractClubMembersInformation(club, nonValidExcelFile)) + .isInstanceOf(InvalidatedEnumValue.class) + .hasMessage("λ™μ•„λ¦¬μ›μ˜ 역할은 LEADER, EXECUTIVE, MEMBER 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€."); + } + + @DisplayName("동아리원 λͺ…단 μ—‘μ…€ νŒŒμΌμ„ μƒμ„±ν•œλ‹€.") + @Test + void generateClubMemberListFile() throws IOException { + //given + List clubMembers = fixtureMonkey.giveMeBuilder(ClubMember.class) + .setNotNull("id") + .setNotNull("name") + .setNotNull("studentNumber") + .setNotNull("phoneNumber") + .setNotNull("position") + .setNotNull("department") + .sampleList(5); + + //when + byte[] excelFileBytes = excelFileService.generateClubMemberListFile(clubMembers); + + //then + try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(excelFileBytes))) { + Sheet sheet = workbook.getSheet("동아리원 λͺ…단"); + assertThat(sheet).isNotNull(); + + // header + Row headerRow = sheet.getRow(0); + assertThat(headerRow.getCell(0).getStringCellValue()).isEqualTo("μ‹λ³„μž(μˆ˜μ •X)"); + assertThat(headerRow.getCell(1).getStringCellValue()).isEqualTo("이름"); + assertThat(headerRow.getCell(2).getStringCellValue()).isEqualTo("ν•™λ²ˆ"); + assertThat(headerRow.getCell(3).getStringCellValue()).isEqualTo("μ—°λ½μ²˜"); + assertThat(headerRow.getCell(4).getStringCellValue()).isEqualTo("비ꡐ(μž„μ›μ§„) - μ˜μ–΄λ§Œ"); + assertThat(headerRow.getCell(5).getStringCellValue()).isEqualTo("ν•™κ³Ό(λΆ€)"); + + CellStyle headerStyle = headerRow.getCell(0).getCellStyle(); + Font font = workbook.getFontAt(headerStyle.getFontIndex()); + assertThat(font.getBold()).isTrue(); + + // data + for (int i = 0; i < clubMembers.size(); i++) { + Row dataRow = sheet.getRow(i + 1); + ClubMember member = clubMembers.get(i); + + assertThat(dataRow.getCell(1).getStringCellValue()).isEqualTo(member.getName()); + assertThat(dataRow.getCell(2).getStringCellValue()).isEqualTo(member.getStudentNumber()); + assertThat(dataRow.getCell(3).getStringCellValue()).isEqualTo(member.getPhoneNumber()); + assertThat(dataRow.getCell(4).getStringCellValue()).isEqualTo(member.getPosition().name()); + assertThat(dataRow.getCell(5).getStringCellValue()).isEqualTo(member.getDepartment()); + } + } + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/file/service/S3FileServiceTest.java b/src/test/java/ddingdong/ddingdongBE/file/service/S3FileServiceTest.java new file mode 100644 index 00000000..4617969a --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/file/service/S3FileServiceTest.java @@ -0,0 +1,73 @@ +package ddingdong.ddingdongBE.file.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Pattern; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +@ActiveProfiles("test") +class S3FileServiceTest { + + @Mock + private AmazonS3Client amazonS3Client; + + @InjectMocks + private S3FileService s3FileService; + + @DisplayName("presignedUrl을 μƒμ„±ν•œλ‹€.") + @Test + void generatePreSignedUrl() throws MalformedURLException { + //given + String fileName = "image.jpg"; + + URL expectedUrl = new URL("https://test-bucket.s3.amazonaws.com/test/jpg/image.jpg"); + given(amazonS3Client.generatePresignedUrl(any(GeneratePresignedUrlRequest.class))).willReturn(expectedUrl); + + //when + UploadUrlResponse uploadUrlResponse = s3FileService.generatePreSignedUrl(fileName); + + //then + Pattern UUID7_PATTERN = Pattern.compile( + "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-7[0-9A-Fa-f]{3}-[89ab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$" + ); + assertThat(uploadUrlResponse.uploadUrl()).isEqualTo(expectedUrl.toString()); + assertThat(Pattern.matches(UUID7_PATTERN.pattern(), uploadUrlResponse.uploadFileName())).isTrue(); + } + + @DisplayName("s3 uploadedFileUrl을 μ‘°νšŒν•œλ‹€.") + @Test + void getUploadedFileUrl() { + //given + String fileName = "image.jpg"; + String uploadFileName = "test"; + + when(amazonS3Client.getRegionName()).thenReturn("ap-northeast-2"); + + ReflectionTestUtils.setField(s3FileService, "bucketName", "test"); + ReflectionTestUtils.setField(s3FileService, "serverProfile", "test"); + + //when + String uploadedFileUrl = s3FileService.getUploadedFileUrl(fileName, uploadFileName); + + //then + Assertions.assertThat(uploadedFileUrl).isEqualTo("https://test.s3.ap-northeast-2.amazonaws.com/test/jpg/test"); + } + +}