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 Developer |
+FE Developer |
+
+
+
+### Back-End
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+κ³ μ μ |
+λ°μν |
+μ μμ€ |
+
+
+BE Developer |
+BE Developer |
+BE 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");
+ }
+
+}
| |