Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c2676c0
Update README.md
2hyunjinn Apr 21, 2025
581ef3e
Update README.md
2hyunjinn Apr 21, 2025
ac38bb5
Update README.md
2hyunjinn Apr 21, 2025
dd5f1cd
[chore] 폴더 구조 리팩토링 (도메인 중심 → 레이어 기반)
2hyunjinn Apr 28, 2025
c53e017
[chore] #62 폴더명 Point -> point 로 변경
2hyunjinn Apr 28, 2025
dab1db3
[chore] #62 테스트코드 폴더링 변경
2hyunjinn Apr 28, 2025
cc77170
Merge pull request #99 from Team-Festimate/refactor/#62
2hyunjinn Apr 28, 2025
1db994f
[refactor] #100 Matching 도메인 리팩토링
2hyunjinn Apr 28, 2025
87cf724
[chore] #100 Matching 관련 테스트 코드 수정
2hyunjinn Apr 28, 2025
edf891f
[refactor] #100 Point 도메인 리팩토링
2hyunjinn Apr 28, 2025
43ac15e
[test] #100 Point 테스트 코드 수정
2hyunjinn Apr 28, 2025
f1ec1d0
[refactor] #100 Participant 도메인 리팩토링
2hyunjinn Apr 28, 2025
02dbace
[test] #100 Participant 테스트 코드 수정
2hyunjinn Apr 28, 2025
95d17ee
[refactor] #100 Participant 관련 dto 파일 폴더 변경
2hyunjinn Apr 28, 2025
fec9a88
[refactor] #100 Festival 도메인 리팩토링
2hyunjinn Apr 28, 2025
7552df0
[test] #100 Festival 관련 테스트 코드 수정
2hyunjinn Apr 28, 2025
79afeb6
[refactor] #100 User 도메인 리팩토링
2hyunjinn Apr 28, 2025
82edac1
[test] #100 User 관련 테스트 코드 수정
2hyunjinn Apr 28, 2025
0348a48
[test] #100 LoginFacade 테스트 코드 구현
2hyunjinn Apr 28, 2025
d154aca
[test] #100 SignUpFacade 테스트 코드 구현
2hyunjinn Apr 28, 2025
38535e5
[del] #100 불필요한 파일 삭제
2hyunjinn Apr 28, 2025
a34be09
[chore] #100 테스트 코드 일부 수정
2hyunjinn Apr 28, 2025
16f6092
Merge pull request #101 from Team-Festimate/refactor/#100
2hyunjinn Apr 28, 2025
ecb7a24
[chore] qa를 위한 cors 처리
2hyunjinn May 1, 2025
748e296
Merge remote-tracking branch 'origin/main'
2hyunjinn May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 26 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,44 +18,43 @@ Festimate는 페스티벌에서 이성과의 네트워킹을 지원하는 **맞
| 이현진 | 👑 **BE 리드 개발자** 👑 <br> Public / Private Subnet 분리 작업 <br> HTTPS 설정 및 도메인 연결 <br> 무중단 배포를 위한 스크립트 작성 | 🧩 **인증 및 회원** <br> - 로그인 API / 로그아웃 API <br> - 회원가입 API <br> - 닉네임 중복확인 API <br><br> 👤 **유저 및 참가자** <br> - 닉네임 조회 API <br> - 참가자 유형 테스트 결과 조회 API / 내 유형 조회 API <br> - 참가자 프로필 생성 API <br> - 전달할 메세지 수정 API <br><br> 🎉 **페스티벌** <br> - 내가 참여하는 페스티벌 조회 API <br> - 페스티벌 초대코드 검증 API / 페스티벌 입장 API <br> - 매칭 추가하기 API / 매칭 리스트 조회 API <br> - 축제 이름 조회 API <br><br> 💰 **포인트 및 어드민 기능** <br> - 페스티벌 생성 API <br> - 페스티벌 참가자 전체 조회 API <br> - 포인트 충전 API / 포인트 내역 조회 API / 특정 유저의 포인트 내역 조회 API <br> - 닉네임 + 포인트 조회 API <br> - 페스티벌 전체 조회 API |

---
## 🏗️ Architecture Overview
![image](https://github.com/user-attachments/assets/6fb54cb2-aa60-41fd-ac98-ce0e6c26a85e)

---
## 🧾 ERD

![festimate-erd (1)](https://github.com/user-attachments/assets/19b1721c-b572-4d7a-a46a-4b64b0a5b463)


## Teck Stack ✨

| 항목 | 내용 |
| --- | --- |
| **IDE** | IntelliJ IDEA |
| **Language** | Java 21 |
| **Framework** | Spring Boot 3.4.3 / Gradle |
| **Build Tool** | Gradle |
| **Authentication** | OAuth 2.0 (Kakao), JSON Web Token (JWT) |
| **Security** | Spring Security |
| **ORM** | Spring Data JPA + Hibernate |
| **Database** | MySQL |
| **Infra/Cloud** | AWS EC2, AWS RDS, Nginx, Route 53 |
| **CI/CD** | GitHub Actions + Docker + Blue-Green Deployment |
| **Monitoring/Logging** | AOP 기반 API 요청 로깅 |
| **Documentation** | Notion (API 명세), ERDCloud (ERD 설계 도구) |
| **API Test** | Postman |
| **Collab Tools** | Discord, Figma, GitHub Projects |
| **Design Tool** | Figma (UI/UX 시안 및 협업) |
---

## API 명세서

<img width="700" alt="image" src="https://github.com/user-attachments/assets/36e010bb-c5e2-416c-8053-c3515730a132" />

[API 명세서 바로가기](https://psychedelic-perigee-94e.notion.site/API-1ceaebccb8e480309a37d1ca2f466a93)

| **HTTP Status** | **Code** | **Message** |
|--------------------------|----------|-----------------------------------------------|
| **400 Bad Request** | 4000 | 잘못된 요청입니다. |
| | 4001 | 유효하지 않은 플랫폼 타입입니다. |
| | 4002 | 요청 파라미터가 잘못되었습니다. |
| | 4003 | 입력된 글자수가 허용된 범위를 벗어났습니다. |
| | 4004 | 닉네임은 한글로만 입력 가능합니다. |
| | 4005 | 유효하지 않은 인가 코드입니다. |
| | 4006 | 유효하지 않은 날짜 형식입니다. |
| **401 Unauthorized** | 4011 | 액세스 토큰의 값이 올바르지 않습니다. |
| | 4012 | 액세스 토큰이 만료되었습니다. 재발급 받아주세요. |
| | 4013 | 초대코드가 만료되었습니다. |
| | 4014 | 페스티벌 기간이 종료되었습니다. |
| | 4015 | 토큰 값이 올바르지 않습니다. |
| **403 Forbidden** | 4030 | 리소스 접근 권한이 없습니다. |
| **404 Not Found** | 4040 | 대상을 찾을 수 없습니다. |
| | 4041 | 존재하지 않는 회원입니다. |
| | 4042 | 존재하지 않는 페스티벌입니다. |
| | 4043 | 존재하지 않는 참가자입니다. |
| **405 Method Not Allowed**| 4050 | 잘못된 HTTP method 요청입니다. |
| **409 Conflict** | 4090 | 이미 존재하는 리소스입니다. |
| | 4091 | 이미 존재하는 회원입니다. |
| | 4092 | 이미 존재하는 참여자입니다. |
| | 4093 | 포인트가 부족합니다. |
| **500 Internal Server Error** | 5000 | 서버 내부 오류입니다. |
---

## 📋 Branch Convention

- `release` : 프로덕트를 배포하는 브랜치입니다.
Expand All @@ -78,7 +77,7 @@ Festimate는 페스티벌에서 이성과의 네트워킹을 지원하는 **맞
- **fix** : 코드 오류 수정 `[fix] #23 회원가입 비즈니스 로직 오류 수정`
- **refactor** : 내부 로직은 변경 하지 않고 기존의 코드를 개선하는 리팩터링 `[refactor] #15 클래스 분리`
- **chore** : 의존성 추가, yml 추가와 수정, 패키지 구조 변경, 파일 이동 등의 작업 `[chore] #30 파일명 변경`
- **test**: 테스트 코드 작성, 수정 `test: 로그인 API 테스트 코드 작성 (#20)`
- **test**: 테스트 코드 작성, 수정 `[test] #20 로그인 API 테스트 코드 작성`

---

Expand Down Expand Up @@ -110,23 +109,6 @@ Festimate는 페스티벌에서 이성과의 네트워킹을 지원하는 **맞
| 8️⃣ **어드민 페이지** | 운영자가 사용자 정보 및 포인트를 실시간으로 관리 | - 관리자 전용 로그인 및 역할 분리<br>- 사용자/포인트/페스티벌 정보 실시간 조회 및 수정 |

---

## Teck Stack ✨

| IDE | IntelliJ |
|------------------|------------------------|
| Language | Java 21 |
| Framework | Spring Boot 3.4.1, Gradle|
| Authentication | JSON Web Tokens |
| ORM | Spring Data JPA |
| Database | MySQL |
| External | AWS EC2, AWS RDS, Nginx|
| CI/CD | Github Action |
| API Docs | Notion |
| Other Tools | Discord, Postman, Figma|

---

## 팀 소개

![image](https://github.com/user-attachments/assets/6cb8fc52-b037-459d-91aa-e233de98d1c1)
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package org.festimate.team.auth.controller;
package org.festimate.team.api.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.festimate.team.common.response.ApiResponse;
import org.festimate.team.common.response.ResponseBuilder;
import org.festimate.team.facade.LoginFacade;
import org.festimate.team.global.jwt.JwtService;
import org.festimate.team.global.jwt.TokenResponse;
import org.festimate.team.user.entity.Platform;
import org.festimate.team.user.service.UserService;
import org.festimate.team.api.auth.dto.TokenResponse;
import org.festimate.team.api.facade.LoginFacade;
import org.festimate.team.domain.user.entity.Platform;
import org.festimate.team.domain.user.service.UserService;
import org.festimate.team.global.response.ApiResponse;
import org.festimate.team.global.response.ResponseBuilder;
import org.festimate.team.infra.jwt.JwtService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand All @@ -30,7 +30,7 @@ public ResponseEntity<ApiResponse<TokenResponse>> login(

String platformId = loginFacade.getPlatformId(kakaoAccessToken);

return userService.getUserIdByPlatformAndPlatformId(Platform.KAKAO, platformId)
return userService.getUserIdByPlatform(Platform.KAKAO, platformId)
.map(userId -> loginFacade.login(platformId, Platform.KAKAO))
.map(ResponseBuilder::ok)
.orElseGet(() -> ResponseBuilder.created(loginFacade.login(platformId, Platform.KAKAO)));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.festimate.team.global.jwt;
package org.festimate.team.api.auth.dto;

public record TokenResponse(Long userId,
String accessToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.festimate.team.common.controller;
package org.festimate.team.api.common;

import org.festimate.team.common.response.ApiResponse;
import org.festimate.team.common.response.ResponseBuilder;
import org.festimate.team.common.response.ResponseError;
import org.festimate.team.exception.FestimateException;
import org.festimate.team.global.response.ApiResponse;
import org.festimate.team.global.response.ResponseBuilder;
import org.festimate.team.global.response.ResponseError;
import org.festimate.team.global.exception.FestimateException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
Expand Down
71 changes: 71 additions & 0 deletions src/main/java/org/festimate/team/api/facade/FestivalFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.festimate.team.api.facade;

import lombok.RequiredArgsConstructor;
import org.festimate.team.api.festival.dto.*;
import org.festimate.team.api.user.dto.UserFestivalResponse;
import org.festimate.team.domain.festival.entity.Festival;
import org.festimate.team.domain.festival.service.FestivalService;
import org.festimate.team.domain.participant.service.ParticipantService;
import org.festimate.team.domain.user.entity.User;
import org.festimate.team.domain.user.service.UserService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Component
@RequiredArgsConstructor
public class FestivalFacade {
private final UserService userService;
private final FestivalService festivalService;
private final ParticipantService participantService;

@Transactional(readOnly = true)
public FestivalVerifyResponse verifyFestival(FestivalVerifyRequest request) {
Festival festival = festivalService.getFestivalByInviteCode(request.inviteCode().trim());
return FestivalVerifyResponse.of(festival);
}

@Transactional(readOnly = true)
public FestivalInfoResponse getFestivalInfo(Long userId, Long festivalId) {
User user = userService.getUserById(userId);
Festival festival = festivalService.getFestivalByIdOrThrow(festivalId);
participantService.validateParticipation(user, festival);
return FestivalInfoResponse.of(festival);
}

@Transactional
public FestivalResponse createFestival(Long userId, FestivalRequest request) {
User host = userService.getUserById(userId);

festivalService.validateCreateFestival(request);

Festival festival = festivalService.createFestival(host, request);
return FestivalResponse.from(festival.getFestivalId(), festival.getInviteCode());
}

@Transactional(readOnly = true)
public List<UserFestivalResponse> getUserFestivals(Long userId, String status) {
User user = userService.getUserById(userId);
return participantService.getFestivalsByUser(user, status)
.stream()
.map(UserFestivalResponse::from)
.toList();
}

@Transactional(readOnly = true)
public List<AdminFestivalResponse> getAllFestivals(Long userId) {
User user = userService.getUserById(userId);
List<Festival> festivals = festivalService.getAllFestivals(user);
return festivals.stream()
.map(AdminFestivalResponse::of)
.toList();
}

@Transactional(readOnly = true)
public AdminFestivalDetailResponse getFestivalDetail(Long userId, Long festivalId) {
Festival festival = festivalService.getFestivalDetailByIdOrThrow(festivalId, userId);
return AdminFestivalDetailResponse.of(festival);
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package org.festimate.team.facade;
package org.festimate.team.api.facade;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.festimate.team.auth.infra.service.KakaoLoginService;
import org.festimate.team.global.jwt.JwtService;
import org.festimate.team.global.jwt.TokenResponse;
import org.festimate.team.user.entity.Platform;
import org.festimate.team.user.service.UserService;
import org.festimate.team.api.auth.dto.TokenResponse;
import org.festimate.team.domain.auth.service.KakaoLoginService;
import org.festimate.team.domain.user.entity.Platform;
import org.festimate.team.domain.user.service.UserService;
import org.festimate.team.infra.jwt.JwtService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -21,12 +21,12 @@ public class LoginFacade {

@Transactional
public TokenResponse login(String platformId, Platform platform) {
return userService.getUserIdByPlatformAndPlatformId(platform, platformId)
.map(this::login)
return userService.getUserIdByPlatform(platform, platformId)
.map(this::loginExistingUser)
.orElseGet(() -> createTemporaryToken(platformId));
}

private TokenResponse login(Long userId) {
private TokenResponse loginExistingUser(Long userId) {
log.info("기존 유저 로그인 성공 - userId: {}", userId);
String newRefreshToken = jwtService.createRefreshToken(userId);

Expand Down
100 changes: 100 additions & 0 deletions src/main/java/org/festimate/team/api/facade/ParticipantFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.festimate.team.api.facade;

import lombok.RequiredArgsConstructor;
import org.festimate.team.api.participant.dto.*;
import org.festimate.team.domain.festival.entity.Festival;
import org.festimate.team.domain.festival.service.FestivalService;
import org.festimate.team.domain.matching.service.MatchingService;
import org.festimate.team.domain.participant.entity.Participant;
import org.festimate.team.domain.participant.service.ParticipantService;
import org.festimate.team.domain.point.service.PointService;
import org.festimate.team.domain.user.entity.User;
import org.festimate.team.domain.user.service.UserService;
import org.festimate.team.global.exception.FestimateException;
import org.festimate.team.global.response.ResponseError;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Component
@RequiredArgsConstructor
public class ParticipantFacade {

private final UserService userService;
private final FestivalService festivalService;
private final ParticipantService participantService;
private final PointService pointService;
private final MatchingService matchingService;

@Transactional(readOnly = true)
public EntryResponse entryFestival(Long userId, Long festivalId) {
User user = userService.getUserById(userId);
Festival festival = festivalService.getFestivalByIdOrThrow(festivalId);
Participant participant = participantService.getParticipant(user, festival);
return EntryResponse.of(participant);
}

@Transactional
public EntryResponse createParticipant(Long userId, Long festivalId, ProfileRequest request) {
User user = userService.getUserById(userId);
Festival festival = festivalService.getFestivalByIdOrThrow(festivalId);

Participant participant = participantService.createParticipant(user, festival, request);
matchingService.matchPendingParticipants(participant);

return EntryResponse.of(participant);
}

@Transactional(readOnly = true)
public ProfileResponse getParticipantProfile(Long userId, Long festivalId) {
Participant participant = getParticipant(userId, festivalId);
Festival festival = participant.getFestival();
return ProfileResponse.of(participant.getTypeResult(), participant.getUser().getNickname(), festival.getMatchingStartTimeStatus());
}

@Transactional(readOnly = true)
public MainUserInfoResponse getParticipantSummary(Long userId, Long festivalId) {
Participant participant = getParticipant(userId, festivalId);
int point = pointService.getTotalPointByParticipant(participant);
return MainUserInfoResponse.from(participant, point, participant.getFestival().getMatchingStartTimeStatus());
}

@Transactional(readOnly = true)
public DetailProfileResponse getParticipantType(Long userId, Long festivalId) {
Participant participant = getParticipant(userId, festivalId);
return DetailProfileResponse.from(participant.getUser(), participant);
}

@Transactional
public void modifyMessage(Long userId, Long festivalId, MessageRequest request) {
Participant participant = getParticipant(userId, festivalId);
participant.modifyIntroductionAndMessage(request.introduction(), request.message());
}

@Transactional(readOnly = true)
public Participant getParticipant(Long userId, Long festivalId) {
User user = userService.getUserById(userId);
Festival festival = festivalService.getFestivalByIdOrThrow(festivalId);
Participant participant = participantService.getParticipant(user, festival);
if (participant == null) {
throw new FestimateException(ResponseError.PARTICIPANT_NOT_FOUND);
}
return participant;
}

@Transactional(readOnly = true)
public List<SearchParticipantResponse> getParticipantByNickname(Long userId, Long festivalId, String nickname) {
User user = userService.getUserById(userId);
Festival festival = festivalService.getFestivalByIdOrThrow(festivalId);
isHost(user, festival);
List<Participant> participants = participantService.getParticipantByNickname(festival, nickname);
return SearchParticipantResponse.from(participants);
}

private void isHost(User user, Festival festival) {
if (!festivalService.isHost(user, festival)) {
throw new FestimateException(ResponseError.FORBIDDEN_RESOURCE);
}
}
}
Loading