diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml new file mode 100644 index 000000000..32902094a --- /dev/null +++ b/.github/workflows/cd-dev.yml @@ -0,0 +1,27 @@ +name: cd-dev + +on: + push: + branches: + - develop-backend + +jobs: + deploy: + runs-on: self-hosted + + steps: + - name: change permission + run: | + sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/2024-mouda + + - name: DockerHub login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Docker Compose up + run: | + docker compose -f ./backend/docker-compose.yml down mouda-be + docker compose -f ./backend/docker-compose.yml pull mouda-be + docker compose -f ./backend/docker-compose.yml up -d mouda-be diff --git a/.github/workflows/cd-frontend.yml b/.github/workflows/cd-frontend.yml new file mode 100644 index 000000000..cc008ff08 --- /dev/null +++ b/.github/workflows/cd-frontend.yml @@ -0,0 +1,37 @@ +name: frontend-deploy + +on: + push: + branches: + - develop-frontend + +jobs: + deploy: + runs-on: self-hosted + + steps: + - name: Log in to Dockerhub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: allow ubuntu to access actions-runner + run: | + sudo chown -R ubuntu:ubuntu ~/actions-runner + + - name: grant permission to docker-compose + run: | + sudo chmod +x ./frontend/docker-compose-fe.yml + + - name: docker compose down + run: | + docker compose -f ./frontend/docker-compose-fe.yml down + + - name: docker compose pull + run: | + docker compose -f ~/frontend/docker-compose-fe.yml pull + + - name: docker compose up + run: | + docker compose -f ~/frontend/docker-compose-fe.yml up -d \ No newline at end of file diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml new file mode 100644 index 000000000..345d7f2f4 --- /dev/null +++ b/.github/workflows/ci-dev.yml @@ -0,0 +1,42 @@ +name: ci-dev + +on: + pull_request: + branches: + - develop-backend + +jobs: + build: + runs-on: ubuntu-24.04 + + defaults: + run: + shell: bash + working-directory: ./backend + + steps: + - name: 레포지토리 체크아웃 + uses: actions/checkout@v4 + + - name: JDK 17을 설치 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: gradlew 권한 부여 + run: chmod +x ./gradlew + + - name: Gradle 빌드 + run: ./gradlew clean build -x test + + - name: DockerHub 로그인 + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: 도커 이미지 빌드 및 푸시 + run: | + docker buildx build ./ --platform=linux/arm64 -t 2024mouda/mouda-be:latest + docker push 2024mouda/mouda-be:latest diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml new file mode 100644 index 000000000..889179d56 --- /dev/null +++ b/.github/workflows/ci-frontend.yml @@ -0,0 +1,49 @@ +name: frontend-integration + +on: + pull_request: + branches: + - develop-frontend + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: develop-frontend + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.15.0' + + - name: Create .env file + run: | + echo "BASE_URL=${{ secrets.BASE_URL }}" > .env + + - name: Install Dependencies + run: npm install --frozen-lockfile + + - name: Build static file + run: npm run build + + - name: Build Docker image + run: | + docker buildx build ./ --platform=linux/arm64 -t ${{ secrets.DOCKER_USERNAME }}/mouda-fe:latest + + - name: Log in to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Push Docker image to DockerHub + run: docker push ${{ secrets.DOCKER_USERNAME }}/mouda-fe:latest \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..5874a9baf --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:17-jdk + +ARG JAR_FILE=./build/libs/backend-0.0.1-SNAPSHOT.jar + +COPY ${JAR_FILE} /app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 000000000..0dbfde141 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.7" # 파일 규격 버전 +services: + mouda-be: + build: . + container_name: mouda-be # 컨테이너 이름 설정 + ports: + - "8080:8080" + command: + - run + - --cors + - "*" diff --git a/backend/src/main/java/mouda/backend/moim/controller/MoimController.java b/backend/src/main/java/mouda/backend/moim/controller/MoimController.java index 392a563ff..6ff48247f 100644 --- a/backend/src/main/java/mouda/backend/moim/controller/MoimController.java +++ b/backend/src/main/java/mouda/backend/moim/controller/MoimController.java @@ -16,6 +16,8 @@ import mouda.backend.common.RestResponse; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.dto.request.MoimCreateRequest; +import mouda.backend.moim.dto.request.MoimJoinRequest; +import mouda.backend.moim.dto.response.MoimDetailsFindResponse; import mouda.backend.moim.dto.response.MoimFindAllResponses; import mouda.backend.moim.service.MoimService; @@ -45,12 +47,31 @@ public ResponseEntity> findAllMoim() { return ResponseEntity.ok().body(new RestResponse<>(moimService.findAllMoim())); } + @Operation(summary = "모임 상세 조회", description = "모임 상세 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 상세 조회 성공!"), + }) + @GetMapping("/{moimId}") + public ResponseEntity> findMoimDetails(@PathVariable long moimId) { + return ResponseEntity.ok().body(new RestResponse<>(moimService.findMoimDetails(moimId))); + } + + @Operation(summary = "모임 참여", description = "모임에 참여한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 참여 성공!") + }) + @PostMapping("/join") + public ResponseEntity> joinMoim(@RequestBody MoimJoinRequest moimJoinRequest) { + moimService.joinMoim(moimJoinRequest); + return ResponseEntity.ok().build(); + } + @Operation(summary = "모임 삭제", description = "해당하는 id의 모임을 삭제한다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "모임 삭제 성공!"), }) - @DeleteMapping("/{id}") - public void deleteMoim(@PathVariable long id) { - moimService.deleteMoim(id); + @DeleteMapping("/{moimId}") + public void deleteMoim(@PathVariable long moimId) { + moimService.deleteMoim(moimId); } } diff --git a/backend/src/main/java/mouda/backend/moim/domain/Moim.java b/backend/src/main/java/mouda/backend/moim/domain/Moim.java index 3f2fa508c..e1ec28e0f 100644 --- a/backend/src/main/java/mouda/backend/moim/domain/Moim.java +++ b/backend/src/main/java/mouda/backend/moim/domain/Moim.java @@ -30,9 +30,18 @@ public class Moim { private String place; + private int currentPeople; + private int maxPeople; private String authorNickname; private String description; + + public void join() { + if (currentPeople + 1 > maxPeople) { + throw new IllegalArgumentException(); + } + currentPeople++; + } } diff --git a/backend/src/main/java/mouda/backend/moim/dto/request/MoimJoinRequest.java b/backend/src/main/java/mouda/backend/moim/dto/request/MoimJoinRequest.java new file mode 100644 index 000000000..cdaa6e296 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/dto/request/MoimJoinRequest.java @@ -0,0 +1,6 @@ +package mouda.backend.moim.dto.request; + +public record MoimJoinRequest( + Long moimId +) { +} diff --git a/backend/src/main/java/mouda/backend/moim/dto/response/MoimDetailsFindResponse.java b/backend/src/main/java/mouda/backend/moim/dto/response/MoimDetailsFindResponse.java new file mode 100644 index 000000000..e62ba6132 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/dto/response/MoimDetailsFindResponse.java @@ -0,0 +1,32 @@ +package mouda.backend.moim.dto.response; + +import java.time.LocalDate; +import java.time.LocalTime; + +import lombok.Builder; +import mouda.backend.moim.domain.Moim; + +@Builder +public record MoimDetailsFindResponse( + String title, + LocalDate date, + LocalTime time, + String place, + int currentPeople, + int maxPeople, + String authorNickname, + String description +) { + public static MoimDetailsFindResponse toResponse(Moim moim) { + return MoimDetailsFindResponse.builder() + .title(moim.getTitle()) + .date(moim.getDate()) + .time(moim.getTime()) + .place(moim.getPlace()) + .currentPeople(moim.getCurrentPeople()) + .maxPeople(moim.getMaxPeople()) + .authorNickname(moim.getAuthorNickname()) + .description(moim.getDescription()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java b/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java index d52c95b82..04978b651 100644 --- a/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java +++ b/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java @@ -8,20 +8,24 @@ @Builder public record MoimFindAllResponse( + long moimId, String title, LocalDate date, LocalTime time, String place, + int currentPeople, int maxPeople, String authorNickname, String description ) { public static MoimFindAllResponse toResponse(Moim moim) { return MoimFindAllResponse.builder() + .moimId(moim.getId()) .title(moim.getTitle()) .date(moim.getDate()) .time(moim.getTime()) .place(moim.getPlace()) + .currentPeople(moim.getCurrentPeople()) .maxPeople(moim.getMaxPeople()) .authorNickname(moim.getAuthorNickname()) .description(moim.getDescription()) diff --git a/backend/src/main/java/mouda/backend/moim/service/MoimService.java b/backend/src/main/java/mouda/backend/moim/service/MoimService.java index ccf2cf6ad..9f54fdc8b 100644 --- a/backend/src/main/java/mouda/backend/moim/service/MoimService.java +++ b/backend/src/main/java/mouda/backend/moim/service/MoimService.java @@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.dto.request.MoimCreateRequest; +import mouda.backend.moim.dto.request.MoimJoinRequest; +import mouda.backend.moim.dto.response.MoimDetailsFindResponse; import mouda.backend.moim.dto.response.MoimFindAllResponse; import mouda.backend.moim.dto.response.MoimFindAllResponses; import mouda.backend.moim.repository.MoimRepository; @@ -33,10 +35,24 @@ public MoimFindAllResponses findAllMoim() { ); } - public void deleteMoim(long id) { + @Transactional(readOnly = true) + public MoimDetailsFindResponse findMoimDetails(long id) { + Moim moim = moimRepository.findById(id) + .orElseThrow(IllegalArgumentException::new); + + return MoimDetailsFindResponse.toResponse(moim); + } + + public void joinMoim(MoimJoinRequest moimJoinRequest) { + Moim moim = moimRepository.findById(moimJoinRequest.moimId()) + .orElseThrow(() -> new IllegalArgumentException("모임이 존재하지 않습니다.")); + moim.join(); + } + + public void deleteMoim(long id) { Moim moim = moimRepository.findById(id) .orElseThrow(IllegalArgumentException::new); moimRepository.delete(moim); - } + } } diff --git a/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java b/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java index 8eb24c3fa..66ca9ca59 100644 --- a/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java +++ b/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java @@ -5,73 +5,107 @@ import java.time.LocalDate; import java.time.LocalTime; import java.util.List; -import mouda.backend.config.DatabaseCleaner; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.dto.request.MoimCreateRequest; -import mouda.backend.moim.dto.response.MoimFindAllResponses; -import mouda.backend.moim.repository.MoimRepository; + import org.junit.jupiter.api.AfterEach; 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 mouda.backend.config.DatabaseCleaner; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.dto.request.MoimCreateRequest; +import mouda.backend.moim.dto.request.MoimJoinRequest; +import mouda.backend.moim.dto.response.MoimDetailsFindResponse; +import mouda.backend.moim.dto.response.MoimFindAllResponses; +import mouda.backend.moim.repository.MoimRepository; + @SpringBootTest class MoimServiceTest { - @Autowired - private MoimService moimService; - - @Autowired - private MoimRepository moimRepository; - - @Autowired - private DatabaseCleaner databaseCleaner; - - @AfterEach - void cleanUp() { - databaseCleaner.cleanUp(); - } - - @DisplayName("모임을 생성한다.") - @Test - void createMoim() { - MoimCreateRequest moimCreateRequest = new MoimCreateRequest( - "title", LocalDate.now(), LocalTime.now(), "place", - 10, "안나", "설명" - ); - Moim moim = moimService.createMoim(moimCreateRequest); - assertThat(moim.getId()).isEqualTo(1L); - } - - @DisplayName("모임을 전체 조회한다.") - @Test - void findAllMoim() { - MoimCreateRequest moimCreateRequest = new MoimCreateRequest( - "title", LocalDate.now(), LocalTime.now(), "place", - 10, "안나", "설명" - ); - moimService.createMoim(moimCreateRequest); - moimService.createMoim(moimCreateRequest); - - MoimFindAllResponses moimResponses = moimService.findAllMoim(); - - assertThat(moimResponses).isNotNull(); - assertThat(moimResponses.moims()).hasSize(2); - } - - @DisplayName("모임을 삭제한다.") - @Test - void deleteMoim() { - MoimCreateRequest moimCreateRequest = new MoimCreateRequest( - "title", LocalDate.now(), LocalTime.now(), "place", - 10, "안나", "설명" - ); - moimService.createMoim(moimCreateRequest); - - moimService.deleteMoim(1L); - List moims = moimRepository.findAll(); - - assertThat(moims).hasSize(0); - } + @Autowired + private MoimService moimService; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private DatabaseCleaner databaseCleaner; + + @AfterEach + void cleanUp() { + databaseCleaner.cleanUp(); + } + + @DisplayName("모임을 생성한다.") + @Test + void createMoim() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now(), LocalTime.now(), "place", + 10, "안나", "설명" + ); + Moim moim = moimService.createMoim(moimCreateRequest); + assertThat(moim.getId()).isEqualTo(1L); + } + + @DisplayName("모임을 전체 조회한다.") + @Test + void findAllMoim() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now(), LocalTime.now(), "place", + 10, "안나", "설명" + ); + moimService.createMoim(moimCreateRequest); + moimService.createMoim(moimCreateRequest); + + MoimFindAllResponses moimResponses = moimService.findAllMoim(); + + assertThat(moimResponses).isNotNull(); + assertThat(moimResponses.moims()).hasSize(2); + } + + @DisplayName("모임 상세를 조회한다.") + @Test + void findMoimDetails() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now(), LocalTime.now(), "place", + 10, "안나", "설명" + ); + moimService.createMoim(moimCreateRequest); + + MoimDetailsFindResponse moimDetails = moimService.findMoimDetails(1L); + + assertThat(moimDetails.authorNickname()).isEqualTo("안나"); + } + + @DisplayName("모임에 참여한다.") + @Test + void joinMoim() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now(), LocalTime.now(), "place", + 10, "안나", "설명" + ); + moimService.createMoim(moimCreateRequest); + + MoimJoinRequest moimJoinRequest = new MoimJoinRequest(1L); + moimService.joinMoim(moimJoinRequest); + + MoimDetailsFindResponse moimDetails = moimService.findMoimDetails(1L); + assertThat(moimDetails.currentPeople()).isEqualTo(1); + } + + @DisplayName("모임을 삭제한다.") + @Test + void deleteMoim() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now(), LocalTime.now(), "place", + 10, "안나", "설명" + ); + moimService.createMoim(moimCreateRequest); + + moimService.deleteMoim(1L); + List moims = moimRepository.findAll(); + + assertThat(moims).hasSize(0); + } } diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..936826696 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,8 @@ +FROM nginx:latest + +COPY dist /usr/share/nginx/html +COPY conf.d /etc/nginx/conf.d + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/conf.d/default.conf b/frontend/conf.d/default.conf new file mode 100644 index 000000000..aa5a6ac3c --- /dev/null +++ b/frontend/conf.d/default.conf @@ -0,0 +1,53 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + location /v1/ { + proxy_pass http://localhost:8080; + proxy_set_header HOST $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-NginX-Proxy true; + proxy_redirect off; + charset utf-8; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} +