Skip to content

Commit 2e460f4

Browse files
authored
Merge pull request #96 from wonseokyoon/develop
Main 병합
2 parents 6e31248 + d920a92 commit 2e460f4

61 files changed

Lines changed: 3173 additions & 154 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ jobs:
4242
4343
- name: Grant execute permission for gradlew
4444
run: chmod +x ./gradlew
45-
4645
- name: application-secret.yml 생성
4746
run: |
4847
echo "$APPLICATION_SECRET" > src/main/resources/application-secret.yml
@@ -56,10 +55,17 @@ jobs:
5655

5756
- name: Docker Buildx 설치
5857
uses: docker/setup-buildx-action@v2
59-
58+
59+
- name: Start services for test
60+
run: docker compose up -d
61+
6062
- name: Run tests with Gradle
6163
run: ./gradlew test
6264

65+
- name: Stop services after test
66+
if: always()
67+
run: docker compose down
68+
6369
# 테스트 실패 시 리포트를 아티팩트로 업로드하여 디버깅을 돕습니다.
6470
- name: Upload test reports
6571
if: failure()
@@ -71,7 +77,7 @@ jobs:
7177
# 2. 릴리스 : main/develop 브랜치로 Push될 때만 실행
7278
makeTagAndRelease:
7379
name: Create Tag and Release
74-
# if: github.event_name == 'push'
80+
if: github.event_name == 'push'
7581
needs: backend-ci
7682
runs-on: ubuntu-latest
7783
permissions:
@@ -101,7 +107,7 @@ jobs:
101107
# 3. 빌드 및 배포: main/develop 브랜치로 Push될 때만 실행
102108
buildImageAndPush:
103109
name: 도커 이미지 빌드와 푸시
104-
# if: github.event_name == 'push'
110+
if: github.event_name == 'push'
105111
needs: makeTagAndRelease
106112
runs-on: ubuntu-latest
107113
steps:
@@ -143,4 +149,4 @@ jobs:
143149
no-cache: true
144150
tags: |
145151
ghcr.io/${{ env.OWNER_LC }}/catch-course:${{ needs.makeTagAndRelease.outputs.tag_name }}
146-
ghcr.io/${{ env.OWNER_LC }}/catch-course:latest
152+
ghcr.io/${{ env.OWNER_LC }}/catch-course:latest

backend/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ dependencies {
7676

7777
// mysql
7878
runtimeOnly 'com.mysql:mysql-connector-j'
79+
80+
// 모니터링
81+
implementation 'org.springframework.boot:spring-boot-starter-actuator'
82+
implementation 'io.micrometer:micrometer-registry-prometheus'
7983
}
8084

8185
tasks.named('test') {

backend/docker-compose-prod.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ services:
1616
redis-prod:
1717
image: redis:latest
1818
container_name: redis-prod
19+
command: ["redis-server", "--notify-keyspace-events", "Ex"]
1920
restart: always
2021
ports:
2122
- "6380:6379"

backend/docker-compose.yml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
version: '3.8'
1+
# docker-compose.yml
22

33
services:
44
redis-dev:
55
image: redis:latest
66
container_name: redis-dev
7+
command: ["redis-server", "--notify-keyspace-events", "Ex"]
78
restart: always
89
ports:
910
- "6379:6379"
@@ -54,6 +55,46 @@ services:
5455
networks:
5556
- dev-network
5657

58+
prometheus-dev:
59+
image: prom/prometheus:latest
60+
container_name: prometheus-dev
61+
restart: always
62+
ports:
63+
- "9090:9090"
64+
volumes:
65+
- ./prometheus.yml:/etc/prometheus/prometheus.yml
66+
command:
67+
- '--config.file=/etc/prometheus/prometheus.yml'
68+
extra_hosts:
69+
- "host.docker.internal:host-gateway"
70+
networks:
71+
- dev-network
72+
73+
grafana-dev:
74+
image: grafana/grafana-oss:latest
75+
container_name: grafana-dev
76+
restart: always
77+
ports:
78+
- "3001:3000"
79+
depends_on:
80+
- prometheus-dev
81+
networks:
82+
- dev-network
83+
84+
node_exporter:
85+
image: quay.io/prometheus/node-exporter:latest
86+
container_name: node_exporter
87+
platform: linux/arm64
88+
restart: unless-stopped
89+
command:
90+
- '--path.rootfs=/host'
91+
volumes:
92+
- '/:/host:ro'
93+
networks:
94+
- dev-network
95+
ports:
96+
- "9100:9100"
97+
5798
networks:
5899
dev-network:
59100
driver: bridge

backend/prometheus.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
global:
2+
scrape_interval: 10s
3+
4+
scrape_configs:
5+
- job_name: 'catch-course-dev'
6+
metrics_path: '/actuator/prometheus'
7+
static_configs:
8+
- targets: ['host.docker.internal:8080']
9+
10+
- job_name: 'node-exporter'
11+
static_configs:
12+
- targets: ['host.docker.internal:9100']

backend/src/main/java/com/Catch_Course/domain/course/controller/CourseController.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public RsData<CourseDto> getItem(@PathVariable long id) {
7373
public RsData<Void> delete(@PathVariable long id) {
7474

7575
Member dummyMember = rq.getDummyMember(); // 더미 유저 객체(id,username,authorities 만 있음, 필요하면 DB에서 꺼내씀)
76-
Course course = courseService.getItem(id)
76+
Course course = courseService.getItemWithPessimisticLock(id)
7777
.orElseThrow(() -> new ServiceException("404-1", "존재하지 않는 강의입니다."));
7878

7979
course.canDelete(dummyMember);
@@ -100,7 +100,7 @@ public RsData<Void> modify(@PathVariable long id, @RequestBody @Valid ModifyReqB
100100
) {
101101

102102
Member dummyMember = rq.getDummyMember();
103-
Course course = courseService.getItem(id)
103+
Course course = courseService.getItemWithPessimisticLock(id)
104104
.orElseThrow(() -> new ServiceException("404-1", "존재하지 않는 강의입니다."));
105105

106106
if (!course.getInstructor().getId().equals(dummyMember.getId())) {
@@ -119,7 +119,8 @@ public RsData<Void> modify(@PathVariable long id, @RequestBody @Valid ModifyReqB
119119
record WriteReqBody(
120120
@NotBlank @Length(min = 3) String title,
121121
@NotBlank @Length(min = 3) String content,
122-
@NotNull @Min(1) long capacity
122+
@NotNull @Min(1) long capacity,
123+
@NotNull @Min(1000) long price
123124
) {
124125
}
125126

@@ -130,7 +131,7 @@ record WriteReqBody(
130131
public RsData<CourseDto> write(@RequestBody @Valid WriteReqBody body) {
131132

132133
Member dummyMember = rq.getDummyMember();
133-
Course course = courseService.write(dummyMember, body.title(), body.content(), body.capacity());
134+
Course course = courseService.write(dummyMember, body.title(), body.content(), body.capacity(),body.price());
134135

135136
return new RsData<>(
136137
"200-1",

backend/src/main/java/com/Catch_Course/domain/course/entity/Course.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class Course extends BaseTime {
2626
private String content; // 강의 내용
2727
private long capacity; // 정원
2828
private long currentRegistration; // 현재 등록 인원
29+
private Long price;
2930

3031
public void canModify(Member member) {
3132
if (member == null) {

backend/src/main/java/com/Catch_Course/domain/course/service/CourseService.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class CourseService {
2222
private final CourseRepository courseRepository;
2323

2424
@Transactional
25-
public Course write(Member member, String title, String content, long capacity) {
25+
public Course write(Member member, String title, String content, long capacity, long price) {
2626

2727
return courseRepository.save(
2828
Course
@@ -32,6 +32,7 @@ public Course write(Member member, String title, String content, long capacity)
3232
.content(content)
3333
.capacity(capacity)
3434
.currentRegistration(0)
35+
.price(price)
3536
.build()
3637
);
3738
}
@@ -58,6 +59,10 @@ public Optional<Course> getItem(long courseId) {
5859
return courseRepository.findById(courseId);
5960
}
6061

62+
public Optional<Course> getItemWithPessimisticLock(long courseId) {
63+
return courseRepository.findByIdWithPessimisticLock(courseId);
64+
}
65+
6166
public long count() {
6267
return courseRepository.count();
6368
}
@@ -76,6 +81,6 @@ public void modify(Course course, String title, String content, long capacity) {
7681

7782
public Course findById(long courseId) {
7883
return courseRepository.findById(courseId)
79-
.orElseThrow(() -> new ServiceException("404-1","존재하지 않는 강의입니다."));
84+
.orElseThrow(() -> new ServiceException("404-1", "존재하지 않는 강의입니다."));
8085
}
8186
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.Catch_Course.domain.payments.controller;
2+
3+
import com.Catch_Course.domain.member.entity.Member;
4+
import com.Catch_Course.domain.payments.dto.PaymentDto;
5+
import com.Catch_Course.domain.payments.service.PaymentService;
6+
import com.Catch_Course.global.Rq;
7+
import com.Catch_Course.global.dto.RsData;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
11+
import jakarta.validation.constraints.NotBlank;
12+
import jakarta.validation.constraints.NotNull;
13+
import lombok.RequiredArgsConstructor;
14+
import org.hibernate.validator.constraints.Length;
15+
import org.springframework.web.bind.annotation.*;
16+
17+
import java.util.List;
18+
19+
@Tag(name = "paymentController", description = "결제 관련 API")
20+
@RestController
21+
@RequestMapping("/api/payment")
22+
@RequiredArgsConstructor
23+
public class PaymentController {
24+
25+
private final PaymentService paymentService;
26+
private final Rq rq;
27+
28+
29+
@Operation(summary = "수강신청 정보로 결제 정보 조회")
30+
@GetMapping("/{reservationId}")
31+
public RsData<PaymentDto> getPayment(@PathVariable Long reservationId) {
32+
33+
Member member = rq.getMember(rq.getDummyMember()); // 실제 멤버 객체
34+
35+
PaymentDto paymentDto = paymentService.getPayment(member, reservationId);
36+
37+
return new RsData<>(
38+
"200-1",
39+
"결제 정보 조회가 완료되었습니다.",
40+
paymentDto
41+
);
42+
}
43+
44+
@Operation(summary = "결제 목록 조회")
45+
@GetMapping()
46+
public RsData<List<PaymentDto>> getPayments() {
47+
48+
Member member = rq.getMember(rq.getDummyMember()); // 실제 멤버 객체
49+
50+
List<PaymentDto> paymentDtos = paymentService.getPayments(member);
51+
52+
return new RsData<>(
53+
"200-1",
54+
"신청 목록 조회가 완료되었습니다.",
55+
paymentDtos
56+
);
57+
}
58+
59+
60+
@Operation(summary = "결제 생성 및 요청")
61+
@PostMapping("/request")
62+
public RsData<PaymentDto> requestPayment(@RequestParam Long reservationId) {
63+
64+
Member member = rq.getMember(rq.getDummyMember()); // 실제 멤버 객체
65+
66+
PaymentDto paymentDto = paymentService.requestPayment(member, reservationId);
67+
68+
return new RsData<>(
69+
"200-1",
70+
"신청 목록 조회가 완료되었습니다.",
71+
paymentDto
72+
);
73+
}
74+
75+
record confirmPaymentReqBody(@NotBlank String paymentKey,
76+
@NotBlank @Length(min = 3) String orderId,
77+
@NotNull Long amount) {
78+
}
79+
80+
@Operation(summary = "결제 승인")
81+
@PostMapping("/confirm")
82+
public RsData<PaymentDto> confirmPayment(@RequestBody @Valid confirmPaymentReqBody body) {
83+
84+
PaymentDto paymentDto = paymentService.confirmPayment(body.paymentKey, body.orderId, body.amount);
85+
86+
return new RsData<>(
87+
"200-1",
88+
"신청 목록 조회가 완료되었습니다.",
89+
paymentDto
90+
);
91+
}
92+
93+
@Operation(summary = "결제 취소")
94+
@DeleteMapping("/{reservationId}")
95+
public RsData<PaymentDto> getReservations(@PathVariable Long reservationId) {
96+
Member member = rq.getMember(rq.getDummyMember()); // 실제 멤버 객체
97+
98+
PaymentDto paymentDto = paymentService.deletePaymentRequest(member, reservationId);
99+
100+
return new RsData<>(
101+
"200-1",
102+
"결제 취소요청이 접수되었습니다.",
103+
paymentDto
104+
);
105+
}
106+
107+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.Catch_Course.domain.payments.controller;
2+
3+
import com.Catch_Course.domain.payments.service.PaymentService;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.annotation.PostMapping;
8+
import org.springframework.web.bind.annotation.RequestBody;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
import java.util.Map;
13+
14+
@RestController
15+
@RequestMapping("/api/webhooks")
16+
@RequiredArgsConstructor
17+
@Slf4j
18+
public class WebhookController {
19+
20+
private final PaymentService paymentService;
21+
22+
// Toss Payments가 보내주는 Webhook 요청을 처리하는 엔드포인트
23+
@PostMapping("/toss")
24+
public ResponseEntity<Void> handleTossWebhook(@RequestBody Map<String, Object> payload) {
25+
log.info("Toss Payments 웹훅 수신: {}", payload);
26+
27+
// Webhook 페이로드에서 이벤트 타입과 주문 ID 추출
28+
String eventType = (String) payload.get("eventType");
29+
Map<String, Object> data = (Map<String, Object>) payload.get("data");
30+
String orderId = (String) data.get("orderId");
31+
32+
// "결제 성공" 이벤트일 때만 처리
33+
if ("PAYMENT_CONFIRMED".equalsIgnoreCase(eventType)) {
34+
paymentService.syncPaymentStatus(orderId);
35+
}
36+
37+
// PG사에게 정상적으로 수신했음을 알림 (HTTP 200 OK)
38+
return ResponseEntity.ok().build();
39+
}
40+
}

0 commit comments

Comments
 (0)