Skip to content
Merged
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
161 changes: 159 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,159 @@
# permit-server
https://www.instagram.com/permit_invites?utm_source=ig_web_button_share_sheet&igsh=ZDNlZDc0MzIxNw==
# ๐ŸŽซ Permit Seoul - ํ‹ฐ์ผ“ ์˜ˆ๋งค ์„œ๋ฒ„

**Permit Seoul**์€ ๊ณต์—ฐ/ํŽ˜์Šคํ‹ฐ๋ฒŒ/ํ–‰์‚ฌ ๋“ฑ์„ ์œ„ํ•œ ํ‹ฐ์ผ“ ์˜ˆ๋งค ์„œ๋น„์Šค์˜ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค.

[![Instagram](https://img.shields.io/badge/Instagram-E4405F?style=flat&logo=instagram&logoColor=white)](https://www.instagram.com/permit_invites)

---

## ๐Ÿ“‹ ๋ชฉ์ฐจ

- [๊ธฐ์ˆ  ์Šคํƒ](#-๊ธฐ์ˆ -์Šคํƒ)
- [์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜](#-์‹œ์Šคํ…œ-์•„ํ‚คํ…์ฒ˜)
- [์ฃผ์š” ๊ธฐ๋Šฅ](#-์ฃผ์š”-๊ธฐ๋Šฅ)
- [ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ](#-ํ”„๋กœ์ ํŠธ-๊ตฌ์กฐ)
- [CI/CD](#cicd)

---

## ๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ

### Backend
| ๊ตฌ๋ถ„ | ๊ธฐ์ˆ  |
|------|------|
| **Language** | Java 17 |
| **Framework** | Spring Boot 3.2.1 |
| **ORM** | Spring Data JPA |
| **Security** | Spring Security, JWT, OAuth2 (Google, Kakao) |
| **Database** | MySQL 8.x |
| **Cache** | Redis |
| **API Client** | Spring Cloud OpenFeign |

### Infrastructure
| ๊ตฌ๋ถ„ | ๊ธฐ์ˆ  |
|------|------|
| **Cloud** | AWS EC2, S3 |
| **Container** | Docker, Amazon Corretto 17 |
| **CI/CD** | GitHub Actions |
| **Logging** | Loki, Logstash, Discord Webhook |
| **Resilience** | Resilience4j (Circuit Breaker) |

### External Services
| ๊ตฌ๋ถ„ | ๊ธฐ์ˆ  |
|------|------|
| **Payment** | Toss Payments API |
| **OAuth** | Google, Kakao ์†Œ์…œ ๋กœ๊ทธ์ธ |
| **Notification** | Email (SMTP), Notion API |
| **QR Code** | ZXing |

---

## ๐Ÿ— ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜

```
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Client โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Nginx โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Spring Boot โ”‚
โ”‚ (Frontend) โ”‚ โ”‚ (Reverse โ”‚ โ”‚ Server โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Proxy) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚
โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ MySQL โ”‚ โ”‚ Redis โ”‚ โ”‚ AWS S3 โ”‚ โ”‚ Toss Payments โ”‚ โ”‚ Google โ”‚ โ”‚ Kakao โ”‚
โ”‚ DB โ”‚ โ”‚ Cache โ”‚ โ”‚ (Images) โ”‚ โ”‚ API โ”‚ โ”‚ OAuth โ”‚ โ”‚ OAuth โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
```

---

## โœจ ์ฃผ์š” ๊ธฐ๋Šฅ

### ๐ŸŽŸ ํ‹ฐ์ผ“ ์˜ˆ๋งค ์‹œ์Šคํ…œ
- **์‹ค์‹œ๊ฐ„ ์žฌ๊ณ  ๊ด€๋ฆฌ**: Redis ๊ธฐ๋ฐ˜ ๋™์‹œ์„ฑ ์ œ์–ด
- **์˜ˆ์•ฝ ์„ธ์…˜ ๊ด€๋ฆฌ**: 7๋ถ„ TTL์˜ ์˜ˆ์•ฝ ์„ธ์…˜์œผ๋กœ ์ขŒ์„ ์ž„์‹œ ํ™•๋ณด
- **์ขŒ์„/ํ‹ฐ์ผ“ ์œ ํ˜• ์„ ํƒ**: ๋‹ค์–‘ํ•œ ํ‹ฐ์ผ“ ํƒ€์ž… ๋ฐ ํšŒ์ฐจ ์ง€์›

### ๐Ÿ’ณ ๊ฒฐ์ œ ์‹œ์Šคํ…œ
- **Toss Payments ์—ฐ๋™**: ๊ฒฐ์ œ ์Šน์ธ ๋ฐ ์ทจ์†Œ API
- **ํŠธ๋žœ์žญ์…˜ ๋ถ„๋ฆฌ ์•„ํ‚คํ…์ฒ˜**: ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ DB ํŠธ๋žœ์žญ์…˜์—์„œ ๋ถ„๋ฆฌํ•˜์—ฌ ์ปค๋„ฅ์…˜ ํšจ์œจ์„ฑ ํ–ฅ์ƒ
- **Resilience4j Circuit Breaker**: ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ์„œํ‚ท ๋ธŒ๋ ˆ์ด์ปค ์ ์šฉ
- **ํ™˜๋ถˆ ์ •์ฑ…**: ํ–‰์‚ฌ ์‹œ์ž‘ 3์ผ ์ „๊นŒ์ง€ ์ทจ์†Œ ๊ฐ€๋Šฅ

### ๐Ÿ‘ค ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ
- **OAuth2 ์†Œ์…œ ๋กœ๊ทธ์ธ**: Google, Kakao
- **JWT ์ธ์ฆ**: Access Token ๊ธฐ๋ฐ˜ ์ธ์ฆ

### ๐ŸŽช ์ด๋ฒคํŠธ/ํ–‰์‚ฌ ๊ด€๋ฆฌ
- **ํ–‰์‚ฌ CRUD**: ๊ด€๋ฆฌ์ž ์ „์šฉ ํ–‰์‚ฌ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ
- **ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ**: Notion API ์—ฐ๋™ ํƒ€์ž„ํ…Œ์ด๋ธ”
- **์ขŒ์„ ๋ฐฐ์น˜๋„**: ์ด๋ฒคํŠธ๋ณ„ ์ขŒ์„ ์ด๋ฏธ์ง€ ๊ด€๋ฆฌ

### ๐Ÿ‘ฅ ๊ฒŒ์ŠคํŠธ ๊ด€๋ฆฌ (Admin)
- **QR ์ฒดํฌ์ธ**: ZXing ๊ธฐ๋ฐ˜ QR ์ฝ”๋“œ ์ƒ์„ฑ ๋ฐ ์Šค์บ”
- **๊ฒŒ์ŠคํŠธ ์ดˆ๋Œ€**: ์ด๋ฉ”์ผ ์ดˆ๋Œ€์žฅ ๋ฐœ์†ก
- **์ž…์žฅ ๊ด€๋ฆฌ**: ์‹ค์‹œ๊ฐ„ ์ฒดํฌ์ธ/์ฒดํฌ์•„์›ƒ

### ๐ŸŽ ์ฟ ํฐ ์‹œ์Šคํ…œ
- **ํ• ์ธ ์ฟ ํฐ**: ์ด๋ฒคํŠธ๋ณ„ ์ฟ ํฐ ๋ฐœ๊ธ‰ ๋ฐ ์ ์šฉ
- **์ฟ ํฐ ์ฝ”๋“œ ์ƒ์„ฑ**: ๊ณ ์œ  ์ฟ ํฐ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ

---

## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

```
src/main/java/com/permitseoul/permitserver/
โ”œโ”€โ”€ PermitServerApplication.java
โ”œโ”€โ”€ domain/ # ๋„๋ฉ”์ธ๋ณ„ ๋ชจ๋“ˆ
โ”‚ โ”œโ”€โ”€ admin/ # ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ
โ”‚ โ”‚ โ”œโ”€โ”€ coupon/ # ์ฟ ํฐ ๊ด€๋ฆฌ
โ”‚ โ”‚ โ”œโ”€โ”€ event/ # ํ–‰์‚ฌ ๊ด€๋ฆฌ
โ”‚ โ”‚ โ”œโ”€โ”€ guest/ # ๊ฒŒ์ŠคํŠธ ๊ด€๋ฆฌ
โ”‚ โ”‚ โ”œโ”€โ”€ ticket/ # ํ‹ฐ์ผ“ ๊ด€๋ฆฌ
โ”‚ โ”‚ โ””โ”€โ”€ timetable/ # ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ
โ”‚ โ”œโ”€โ”€ auth/ # ์ธ์ฆ/์ธ๊ฐ€
โ”‚ โ”œโ”€โ”€ coupon/ # ์ฟ ํฐ
โ”‚ โ”œโ”€โ”€ event/ # ํ–‰์‚ฌ
โ”‚ โ”œโ”€โ”€ payment/ # ๊ฒฐ์ œ
โ”‚ โ”œโ”€โ”€ reservation/ # ์˜ˆ์•ฝ
โ”‚ โ”œโ”€โ”€ reservationsession/ # ์˜ˆ์•ฝ ์„ธ์…˜
โ”‚ โ”œโ”€โ”€ ticket/ # ํ‹ฐ์ผ“
โ”‚ โ””โ”€โ”€ user/ # ์‚ฌ์šฉ์ž
โ””โ”€โ”€ global/ # ๊ณตํ†ต ๋ชจ๋“ˆ
โ”œโ”€โ”€ aop/ # AOP ์„ค์ •
โ”œโ”€โ”€ config/ # ์„ค์ • ํด๋ž˜์Šค
โ”œโ”€โ”€ exception/ # ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
โ”œโ”€โ”€ external/ # ์™ธ๋ถ€ API ์—ฐ๋™
โ”œโ”€โ”€ filter/ # ํ•„ํ„ฐ
โ”œโ”€โ”€ redis/ # Redis ์„ค์ •
โ”œโ”€โ”€ response/ # ์‘๋‹ต ๊ฐ์ฒด
โ””โ”€โ”€ util/ # ์œ ํ‹ธ๋ฆฌํ‹ฐ
```

---

## CI/CD

GitHub Actions๋ฅผ ํ†ตํ•œ ์ž๋™ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ:

```
main ๋ธŒ๋žœ์น˜ Push
โ†“
GitHub Actions
โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ 1. Checkout โ”‚
โ”‚ 2. Set up JDK 17 โ”‚
โ”‚ 3. Build (Gradle) โ”‚
โ”‚ 4. Docker Build & Push โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ†“
Docker Hub
โ†“
EC2 SSH Deploy
```

### ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ
- `CI-PROD.yml` / `CI_DEV.yml`: CI ํŒŒ์ดํ”„๋ผ์ธ
- `DOCKER-PROD-CD.yml` / `DOCKER-DEV-CD.yml`: CD ํŒŒ์ดํ”„๋ผ์ธ
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException;
import com.permitseoul.permitserver.global.response.code.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class AdminGuestTicketService {
private final AdminGuestRetriever adminGuestRetriever;
private final GuestTicketEmailSender guestTicketEmailSender;
Expand Down Expand Up @@ -53,6 +55,9 @@ public void issueGuestTickets(final long eventId, final List<GuestTicketIssueReq
throw new AdminGuestTicketApiException(ErrorCode.NOT_FOUND_EVENT);
} catch (GuestTicketNotFoundException e) {
throw new AdminGuestTicketApiException(ErrorCode.NOT_FOUND_GUEST_TICKET);
} catch (Exception e) {
log.error("[Guest Ticket Email] ๋ฐœ์†ก ์‹คํŒจ - ์ด๋ฒคํŠธ์•„์ด๋””:{}, ์ˆ˜์‹ ์ž ์ •๋ณด : {}", eventId, guestTicketList, e);
throw new AdminGuestTicketApiException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.mail.javamail.JavaMailSender;
Expand All @@ -21,6 +22,7 @@

@Component("guestTicketEmailSender")
@RequiredArgsConstructor
@Slf4j
public class GuestTicketEmailSender {
private final JavaMailSender mailSender;
private final TemplateEngine templateEngine;
Expand Down Expand Up @@ -64,6 +66,8 @@ public void sendGuestTicketsEmail(
}

mailSender.send(mimeMessage);
log.info("[Guest Ticket Email] ๋ฐœ์†ก ์™„๋ฃŒ - ์ˆ˜์‹ ์ž: {} ({}), ์ด๋ฒคํŠธ: {}, ํ‹ฐ์ผ“ ์ˆ˜: {}",
guestName, toEmail, eventName, ticketCodes.size());
} catch (MessagingException | UnsupportedEncodingException e) {
throw new EmailSendException(ErrorCode.INTERNAL_EMAIL_SEND_ERROR);
}
Expand Down