Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,59 @@
# TagCafe-BE
# TagCafe Backend

**카공에 진심인 당신을 위한 카페 플랫폼, TagCafe**

와이파이, 책상 크기, 콘센트 등 카공에 중요한 요소들을 태그 기반으로 필터링하고,
리뷰와 제보를 통해 사용자에게 딱 맞는 카페를 찾아주는 서비스입니다.

---

## 🌐 배포 주소

- 웹 서비스: [https://tagcafe.site](https://tagcafe.site)
- Swagger API 문서: [https://tagcafe.site/swagger-ui/index.html](https://tagcafe.site/swagger-ui/index.html)

<br>

## 🧑‍💻 역할

| 이름 | 역할 |
|:----:|----------------------------------------------------------------|
| <div align="center"><a href="https://github.com/ghi512"><img src="https://avatars.githubusercontent.com/ghi512" width="100"/><br/>김민지</a></div> | 카페 검색 및 조회, 태그 필터링, 카페 저장 기능 개발<br/>Swagger를 활용한 전체 API 문서화 |
| <div align="center"><a href="https://github.com/jjinleee"><img src="https://avatars.githubusercontent.com/jjinleee" width="100"/><br/>이진</a></div> | 회원 관리, 마이페이지(리뷰/제보) 기능 구현<br/>카카오 로그인 연동<br/>CI/CD 설정 및 배포 자동화 |

<br>

## 🛠 기술 스택

| 항목 | 내용 |
|-------------|------------------------------------------------------------------|
| 언어 | Java 17 |
| 프레임워크 | Spring Boot 3.4.2 |
| ORM | Spring Data JPA |
| 빌드 도구 | Gradle |
| DB | MySQL 8.0 (AWS RDS) |
| 인증 | Spring Security + Kakao OAuth 2.0 + JWT |
| 배포 | Docker + Docker Compose + NGINX |
| CI/CD | GitHub Actions → EC2 자동 배포 |
| 웹서버 | NGINX (React + API 리버스 프록시) |
| 인증서 | Let’s Encrypt (HTTPS 인증서 적용) |
| 환경 변수 | dotenv-java 기반 `.env`, `application.yml` 에서 외부 주입 |
| API 문서화 | Swagger (springdoc-openapi) |

<br>

## 📁 프로젝트 구조

```bash
📦 TagCafe
┣ 📂 config # 전역 설정 (보안, Swagger, 카카오 OAuth 등)
┣ 📂 controller # API 컨트롤러
┣ 📂 dto # 요청/응답 DTO
┣ 📂 entity # JPA 엔티티
┣ 📂 repository # DB 접근 (JPA 레포지토리)
┣ 📂 service # 비즈니스 로직 처리
┣ 📂 util # 공통 유틸 기능 (JWT)
┗ TagCafeApplication.java # 메인 클래스
```


1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.Minjin.TagCafe.entity.User;
import com.Minjin.TagCafe.repository.UserRepository;
import com.Minjin.TagCafe.util.JwtUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -21,6 +23,7 @@
import java.util.Map;
import java.util.Optional;

@Tag(name = "Auth", description = "카카오 로그인 및 사용자 인증 관련 API")
@RestController
@RequestMapping("/oauth/kakao")
@RequiredArgsConstructor
Expand All @@ -33,6 +36,7 @@ public class KakaoAuthController {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;

@Operation(summary = "카카오 로그인 시작", description = "카카오 로그인 인증 페이지로 리디렉션합니다.")
@GetMapping("/login")
public RedirectView kakaoLogin() {
String kakaoAuthUrl = "https://kauth.kakao.com/oauth/authorize"
Expand All @@ -44,6 +48,7 @@ public RedirectView kakaoLogin() {
return new RedirectView(kakaoAuthUrl);
}

@Operation(summary = "카카오 로그인 콜백", description = "카카오 인증 후 사용자 정보를 받아옵니다.")
@GetMapping("/callback")
public RedirectView kakaoCallback(@RequestParam(name="code") String code) {

Expand Down Expand Up @@ -119,6 +124,7 @@ public RedirectView kakaoCallback(@RequestParam(name="code") String code) {
+ "&token=" + jwtToken);
}

@Operation(summary = "로그인한 사용자 정보 조회", description = "현재 로그인한 사용자의 정보를 반환합니다.")
@GetMapping("/userinfo")
public ResponseEntity<String> getUserInfo(@RequestParam("access_token") String accessToken) {
String userInfoUrl = "https://kapi.kakao.com/v2/user/me";
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/Minjin/TagCafe/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.Minjin.TagCafe.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("TagCafe API")
.description("카공러 맞춤형 카페 정보 플랫폼 TagCafe의 API 명세서입니다.")
.version("v1.0.0"));
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/Minjin/TagCafe/controller/CafeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.Minjin.TagCafe.entity.Cafe;
import com.Minjin.TagCafe.repository.CafeRepository;
import com.Minjin.TagCafe.service.CafeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -15,6 +17,7 @@
import java.util.*;
import java.util.stream.Collectors;

@Tag(name = "Cafe", description = "카페 정보 조회 및 필터 검색 API")
@RestController
@RequestMapping("/cafes")
@RequiredArgsConstructor
Expand All @@ -24,6 +27,7 @@ public class CafeController {
private final CafeRepository cafeRepository;

// id로 카페 조회
@Operation(summary = "ID로 카페 조회", description = "카페 ID를 기반으로 카페 상세 정보를 조회합니다.")
@GetMapping("/{cafeId}")
public ResponseEntity<CafeDto> getCafeById(@PathVariable("cafeId") Long cafeId) {
Cafe cafe = cafeService.getCafeById(cafeId);
Expand Down Expand Up @@ -54,6 +58,7 @@ public ResponseEntity<CafeDto> getCafeById(@PathVariable("cafeId") Long cafeId)
}

// 검색
@Operation(summary = "카페 검색", description = "키워드로 카페를 검색합니다.")
@GetMapping("/search")
public ResponseEntity<List<CafeSearchDTO>> searchCafe(@RequestParam(name = "query") String query) {
List<Cafe> cafes = cafeService.searchCafeByKeyword(query);
Expand All @@ -72,6 +77,7 @@ public ResponseEntity<List<CafeSearchDTO>> searchCafe(@RequestParam(name = "quer
}

// 지도 영역 내 카페 조회 (위경도 범위 내 검색)
@Operation(summary = "지도의 특정 영역 내 카페 조회", description = "지도 상에서 특정 위경도 범위에 있는 카페 목록을 반환합니다.")
@GetMapping("/area")
public ResponseEntity<List<CafeHomeDTO>> getCafesInArea(@RequestParam(name = "minLat") double minLat,
@RequestParam(name = "maxLat") double maxLat,
Expand Down Expand Up @@ -105,6 +111,7 @@ public ResponseEntity<List<CafeHomeDTO>> getCafesInArea(@RequestParam(name = "mi
}

// 특정 태그와 특정 값을 가진 카페 조회
@Operation(summary = "여러 태그 조건으로 카페 필터링", description = "태그 이름과 값들을 기준으로 카페를 필터링하여 조회합니다.")
@GetMapping("/filter")
public ResponseEntity<List<CafeHomeDTO>> getCafesByMultipleTags(@RequestParam(name = "tagNames") List<String> tagNames,
@RequestParam(name = "values") List<String> values) {
Expand Down Expand Up @@ -152,13 +159,15 @@ public ResponseEntity<List<CafeHomeDTO>> getCafesByMultipleTags(@RequestParam(na


// admin - 카페 검색 후 db 저장
@Operation(summary = "admin - 카페 저장", description = "관리자가 카페 정보를 저장합니다.")
@PostMapping
public ResponseEntity<?> addCafe(@RequestBody CafeDto cafeDto) {
Cafe savedCafe = cafeService.addCafe(cafeDto);
return ResponseEntity.ok(savedCafe);
}

// admin - 태그 값 업데이트
@Operation(summary = "admin - 카페 태그 수정", description = "특정 카페의 태그 정보(wifi, 책상 등)를 수정합니다.")
@PutMapping("/{cafeId}/tags")
public ResponseEntity<Cafe> updateCafeTags(@PathVariable("cafeId") Long cafeId,
@RequestBody CafeDto cafeDto) {
Expand All @@ -176,12 +185,14 @@ public ResponseEntity<Cafe> updateCafeTags(@PathVariable("cafeId") Long cafeId,
}

// 모든 카페 조회 API 추가
@Operation(summary = "모든 카페 조회", description = "전체 카페 목록을 조회합니다.")
@GetMapping
public ResponseEntity<List<Cafe>> getAllCafes() {
List<Cafe> cafes = cafeService.getAllCafes();
return ResponseEntity.ok(cafes);
}

@Operation(summary = "카페 태그 조회", description = "카페 ID를 기준으로 태그(wifi, 콘센트 등) 정보를 조회합니다.")
@GetMapping("/{cafeId}/tags")
public ResponseEntity<Map<String, String>> getCafeTags(@PathVariable("cafeId") Long cafeId) {
Cafe cafe = cafeRepository.findById(cafeId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.Minjin.TagCafe.entity.QA;
import com.Minjin.TagCafe.repository.FeedbackRepository;
import com.Minjin.TagCafe.repository.QARepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand All @@ -16,6 +18,7 @@
import java.util.Map;
import java.util.stream.Collectors;

@Tag(name = "FAQ", description = "자주 묻는 질문 및 사용자 피드백 관련 API")
@RestController
@RequestMapping("/faq")
@CrossOrigin(origins = "http://tagcafe.site")
Expand All @@ -30,13 +33,15 @@ public FAQController(FeedbackRepository feedbackRepository, QARepository qaRepos


//자주 묻는 질문 조회
@Operation(summary = "자주 묻는 질문 조회", description = "모든 QA 데이터를 조회합니다.")
@GetMapping("/qa")
public ResponseEntity<List<QA>> getQAs() {
List<QA> qaList = qaRepository.findAll();
return ResponseEntity.ok(qaList);
}

//사용자 의견 및 오류 제보 제출
@Operation(summary = "사용자 피드백 제출", description = "사용자의 의견이나 오류 제보를 저장합니다.")
@PostMapping("/feedback")
public ResponseEntity<Map<String, String>> submitFeedback(@RequestBody FeedbackRequest request) {
Feedback feedback = new Feedback();
Expand All @@ -51,6 +56,7 @@ public ResponseEntity<Map<String, String>> submitFeedback(@RequestBody FeedbackR
}

// ✅ 관리자용 - 사용자 의견 및 오류 목록 조회 API
@Operation(summary = "admin - 사용자 피드백 전체 조회", description = "관리자가 보는 사용자 피드백 목록입니다.")
@GetMapping("/feedback")
public ResponseEntity<List<FeedbackResponse>> getFeedbackList() {
List<FeedbackResponse> feedbacks = feedbackRepository.findAll()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.Minjin.TagCafe.controller;

import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.view.RedirectView;

@Hidden
@RestController
public class HomeController {
@GetMapping("/")
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/Minjin/TagCafe/controller/MyController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.Minjin.TagCafe.entity.Cafe;
import com.Minjin.TagCafe.repository.CafeRepository;
import com.Minjin.TagCafe.service.ReviewService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
Expand All @@ -14,6 +16,7 @@
import java.util.List;
import java.util.Map;

@Tag(name = "My", description = "마이페이지 - 내가 작성한 리뷰 조회 및 관리 API")
@RestController
@RequestMapping("/my")
public class MyController {
Expand All @@ -23,6 +26,7 @@ public class MyController {
@Autowired
private CafeRepository cafeRepository;

@Operation(summary = "내가 작성한 리뷰 목록 조회", description = "userEmail을 기반으로 사용자가 작성한 리뷰 목록을 조회합니다.")
@GetMapping("/reviews")
public ResponseEntity<List<ReviewDTO>> getUserReviews(
@RequestHeader("Authorization") String authorizationHeader,
Expand All @@ -40,6 +44,7 @@ public ResponseEntity<List<ReviewDTO>> getUserReviews(
return ResponseEntity.ok(reviews);
}

@Operation(summary = "리뷰 상세 조회", description = "리뷰 ID를 기반으로 해당 리뷰의 상세 정보와 카페 정보를 조회합니다.")
@GetMapping("/reviews/{reviewId}")
public ResponseEntity<Map<String, Object>> getReviewById(@PathVariable("reviewId") Long reviewId) {
if (reviewId == null) {
Expand All @@ -65,12 +70,14 @@ public ResponseEntity<Map<String, Object>> getReviewById(@PathVariable("reviewId
}
}

@Operation(summary = "리뷰 수정", description = "리뷰 ID를 기반으로 내용을 수정합니다.")
@PutMapping("/reviews/{reviewId}")
public ResponseEntity<String> updateReview(@PathVariable("reviewId") Long reviewId, @RequestBody ReviewDTO dto) {
reviewService.updateReview(reviewId, dto);
return ResponseEntity.ok("리뷰가 성공적으로 수정되었습니다.");
}

@Operation(summary = "리뷰 삭제", description = "리뷰 ID를 기반으로 리뷰를 삭제합니다.")
@DeleteMapping("/reviews/{reviewId}")
public ResponseEntity<String> deleteReview(@PathVariable("reviewId") Long reviewId) {
try {
Expand All @@ -80,6 +87,8 @@ public ResponseEntity<String> deleteReview(@PathVariable("reviewId") Long review
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("리뷰 삭제 실패");
}
}

@Operation(summary = "태그 기반 리뷰 필터 조회", description = "userEmail과 선택한 태그 조건을 바탕으로 리뷰를 필터링하여 조회합니다.")
@GetMapping("/reviews/filter")
public ResponseEntity<List<ReviewDTO>> getReviewsByCafeWithFilter(
@RequestParam("userEmail") String userEmail,
Expand Down
Loading