Skip to content

Commit 2db0841

Browse files
authored
Merge pull request #107 from IT-Cotato/feature/105
feat: 주문 취소/배송지 변경 API 구현 및 HTTPS 도메인
2 parents 2a84f34 + a20cb9b commit 2db0841

File tree

23 files changed

+612
-40
lines changed

23 files changed

+612
-40
lines changed

docker-compose.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ services:
55
image: nginx:alpine
66
container_name: ongil-nginx
77
ports:
8-
- "80:80"
8+
- "80:80" # HTTP (암호화 안 됨)
9+
- "443:443" # HTTPS (암호화됨, SSL/TLS)
910
volumes:
10-
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
11+
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
12+
- /etc/letsencrypt:/etc/letsencrypt:ro
1113
depends_on:
1214
- backend
1315
restart: always
@@ -56,4 +58,4 @@ services:
5658
- "9200:9200"
5759
volumes:
5860
- /home/ubuntu/ongil-backend/es_data:/usr/share/elasticsearch/data
59-
restart: always
61+
restart: always

nginx/nginx.conf

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,79 @@
1-
server {
2-
listen 80;
3-
server_name _;
4-
5-
location / {
6-
proxy_pass http://ongil-backend:8080;
7-
proxy_set_header Host $host;
8-
proxy_set_header X-Real-IP $remote_addr;
9-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
10-
proxy_set_header X-Forwarded-Proto $scheme;
11-
12-
proxy_connect_timeout 60;
13-
proxy_send_timeout 300;
14-
proxy_read_timeout 300;
1+
events {
2+
worker_connections 1024;
3+
}
4+
5+
http {
6+
include /etc/nginx/mime.types;
7+
default_type application/octet-stream;
8+
9+
# HTTP → HTTPS 리다이렉트(강제 이동)
10+
server {
11+
listen 80;
12+
server_name ongil.shop www.ongil.shop;
13+
14+
location / {
15+
return 301 https://$host$request_uri;
16+
}
17+
}
18+
19+
# HTTPS 서버 및 인증서 설정
20+
server {
21+
listen 443 ssl;
22+
server_name ongil.shop www.ongil.shop;
23+
24+
# 아까 Certbot으로 발급받은 '신분증'과 '비밀키' 위치
25+
ssl_certificate /etc/letsencrypt/live/ongil.shop/fullchain.pem;
26+
ssl_certificate_key /etc/letsencrypt/live/ongil.shop/privkey.pem;
27+
28+
# 백엔드 API
29+
location /api/ {
30+
# [중요] Docker 컨테이너 이름(ongil-backend)으로 통신
31+
proxy_pass http://ongil-backend:8080;
32+
proxy_set_header Host $host;
33+
proxy_set_header X-Real-IP $remote_addr;
34+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
35+
proxy_set_header X-Forwarded-Proto $scheme;
36+
proxy_connect_timeout 60;
37+
proxy_send_timeout 300;
38+
proxy_read_timeout 300;
39+
}
40+
41+
# Swagger UI - 정확한 경로 리다이렉트
42+
location = /swagger-ui/ {
43+
return 301 https://$host/swagger-ui/index.html;
44+
}
45+
46+
# Swagger가 필요로 하는 CSS, JS 파일 등을 백엔드에서 가져옴
47+
location = /swagger-ui {
48+
return 301 https://$host/swagger-ui/index.html;
49+
}
50+
51+
# Swagger UI 리소스 (CSS, JS 등)
52+
location /swagger-ui/ {
53+
proxy_pass http://ongil-backend:8080/swagger-ui/;
54+
proxy_set_header Host $host;
55+
proxy_set_header X-Real-IP $remote_addr;
56+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
57+
proxy_set_header X-Forwarded-Proto $scheme;
58+
}
59+
60+
# API Docs
61+
location /v3/api-docs {
62+
proxy_pass http://ongil-backend:8080/v3/api-docs;
63+
proxy_set_header Host $host;
64+
proxy_set_header X-Real-IP $remote_addr;
65+
}
66+
67+
location /v3/ {
68+
proxy_pass http://ongil-backend:8080/v3/;
69+
proxy_set_header Host $host;
70+
proxy_set_header X-Real-IP $remote_addr;
71+
}
72+
73+
# 프론트엔드 (임시 기본 페이지)
74+
location / {
75+
root /usr/share/nginx/html;
76+
try_files $uri $uri/ /index.html;
77+
}
1578
}
1679
}

src/main/java/com/ongil/backend/domain/address/controller/AddressController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111

1212
import com.ongil.backend.domain.address.dto.request.ShippingInfoCreateReqDto;
1313
import com.ongil.backend.domain.address.dto.request.ShippingInfoUpdateReqDto;
14+
import com.ongil.backend.domain.address.dto.response.AddressListResponse;
1415
import com.ongil.backend.domain.address.dto.response.ShippingInfoResDto;
1516
import com.ongil.backend.domain.address.service.AddressService;
1617
import com.ongil.backend.global.common.dto.DataResponse;
1718

19+
import java.util.List;
20+
1821
import io.swagger.v3.oas.annotations.Operation;
1922
import io.swagger.v3.oas.annotations.tags.Tag;
2023
import jakarta.validation.Valid;
@@ -28,6 +31,14 @@ public class AddressController {
2831

2932
private final AddressService addressService;
3033

34+
@GetMapping
35+
@Operation(summary = "내 배송지 목록 조회", description = "현재 로그인한 사용자의 전체 배송지 목록을 조회합니다. 토큰 필요")
36+
public DataResponse<List<AddressListResponse>> getAddressList(
37+
@AuthenticationPrincipal Long userId
38+
) {
39+
return DataResponse.from(addressService.getAddressList(userId));
40+
}
41+
3142
@GetMapping("/me")
3243
@Operation(summary = "내 배송지 조회", description = "현재 로그인한 사용자의 배송지 정보를 조회합니다.")
3344
public DataResponse<ShippingInfoResDto> getShippingInfo(
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.ongil.backend.domain.address.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
@Schema(description = "배송지 목록 항목")
6+
public record AddressListResponse(
7+
8+
@Schema(description = "배송지 ID")
9+
Long addressId,
10+
11+
@Schema(description = "수령인 이름")
12+
String recipientName,
13+
14+
@Schema(description = "수령인 연락처")
15+
String recipientPhone,
16+
17+
@Schema(description = "기본 주소")
18+
String baseAddress,
19+
20+
@Schema(description = "상세 주소")
21+
String detailAddress,
22+
23+
@Schema(description = "우편번호")
24+
String postalCode,
25+
26+
@Schema(description = "기본 배송지 여부")
27+
boolean isDefault
28+
) {
29+
}

src/main/java/com/ongil/backend/domain/address/repository/AddressRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.ongil.backend.domain.address.repository;
22

3+
import java.util.List;
34
import java.util.Optional;
45

56
import org.springframework.data.jpa.repository.JpaRepository;
@@ -10,5 +11,7 @@ public interface AddressRepository extends JpaRepository<Address, Long> {
1011

1112
Optional<Address> findFirstByUserIdOrderByCreatedAtDesc(Long userId);
1213

14+
List<Address> findAllByUserIdOrderByIsDefaultDescCreatedAtDesc(Long userId);
15+
1316
void deleteAllByUserId(Long userId);
1417
}

src/main/java/com/ongil/backend/domain/address/service/AddressService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.ongil.backend.domain.address.service;
22

3+
import java.util.List;
34
import java.util.Optional;
45

56
import org.springframework.stereotype.Service;
@@ -8,6 +9,7 @@
89
import com.ongil.backend.domain.address.converter.AddressConverter;
910
import com.ongil.backend.domain.address.dto.request.ShippingInfoCreateReqDto;
1011
import com.ongil.backend.domain.address.dto.request.ShippingInfoUpdateReqDto;
12+
import com.ongil.backend.domain.address.dto.response.AddressListResponse;
1113
import com.ongil.backend.domain.address.dto.response.ShippingInfoResDto;
1214
import com.ongil.backend.domain.address.entity.Address;
1315
import com.ongil.backend.domain.address.repository.AddressRepository;
@@ -27,6 +29,22 @@ public class AddressService {
2729
private final AddressRepository addressRepository;
2830
private final UserRepository userRepository;
2931

32+
public List<AddressListResponse> getAddressList(Long userId) {
33+
List<Address> addresses = addressRepository.findAllByUserIdOrderByIsDefaultDescCreatedAtDesc(userId);
34+
35+
return addresses.stream()
36+
.map(address -> new AddressListResponse(
37+
address.getId(),
38+
address.getRecipientName(),
39+
address.getRecipientPhone(),
40+
address.getBaseAddress(),
41+
address.getDetailAddress(),
42+
address.getPostalCode(),
43+
address.isDefault()
44+
))
45+
.toList();
46+
}
47+
3048
public ShippingInfoResDto getShippingInfo(Long userId) {
3149
Optional<Address> address = addressRepository.findFirstByUserIdOrderByCreatedAtDesc(userId);
3250

src/main/java/com/ongil/backend/domain/order/controller/OrderController.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@
88
import org.springframework.format.annotation.DateTimeFormat;
99
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1010
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PatchMapping;
1112
import org.springframework.web.bind.annotation.PathVariable;
1213
import org.springframework.web.bind.annotation.PostMapping;
1314
import org.springframework.web.bind.annotation.RequestBody;
1415
import org.springframework.web.bind.annotation.RequestMapping;
1516
import org.springframework.web.bind.annotation.RequestParam;
1617
import org.springframework.web.bind.annotation.RestController;
1718

19+
import com.ongil.backend.domain.order.dto.request.CancelRefundInfoRequest;
1820
import com.ongil.backend.domain.order.dto.request.CartOrderRequest;
21+
import com.ongil.backend.domain.order.dto.request.DeliveryAddressUpdateRequest;
22+
import com.ongil.backend.domain.order.dto.request.OrderCancelRequest;
1923
import com.ongil.backend.domain.order.dto.request.OrderCreateRequest;
24+
import com.ongil.backend.domain.order.dto.response.CancelRefundInfoResponse;
25+
import com.ongil.backend.domain.order.dto.response.OrderCancelResponse;
2026
import com.ongil.backend.domain.order.dto.response.OrderDetailResponse;
2127
import com.ongil.backend.domain.order.dto.response.OrderHistoryResponse;
2228
import com.ongil.backend.domain.order.service.OrderService;
@@ -108,4 +114,43 @@ public DataResponse<OrderDetailResponse> getOrderDetail(
108114
return DataResponse.from(orderService.getOrderDetail(userId, orderId));
109115
}
110116

117+
@GetMapping("/{orderId}/cancel/refund-info")
118+
@Operation(
119+
summary = "환불 정보 조회",
120+
description = "주문 취소 시 환불 정보를 조회합니다. 주문 접수 상태에서만 조회 가능합니다. 토큰 필요"
121+
)
122+
public DataResponse<CancelRefundInfoResponse> getRefundInfo(
123+
@AuthenticationPrincipal Long userId, @PathVariable Long orderId
124+
) {
125+
return DataResponse.from(orderService.getRefundInfo(userId, orderId));
126+
}
127+
128+
@PostMapping("/{orderId}/cancel")
129+
@Operation(
130+
summary = "주문 취소",
131+
description = "주문을 취소합니다. 주문 접수 상태에서만 취소 가능합니다. " +
132+
"addToCart가 true이면 취소된 상품을 장바구니에 다시 담습니다. 토큰 필요"
133+
)
134+
public DataResponse<OrderCancelResponse> cancelOrder(
135+
@AuthenticationPrincipal Long userId,
136+
@PathVariable Long orderId,
137+
@RequestBody @Valid OrderCancelRequest request
138+
) {
139+
return DataResponse.from(orderService.cancelOrder(userId, orderId, request));
140+
}
141+
142+
@PatchMapping("/{orderId}/delivery-address")
143+
@Operation(
144+
summary = "주문 배송지 변경",
145+
description = "주문의 배송지를 사용자의 기존 등록 배송지 중 하나로 변경합니다. " +
146+
"주문 접수 상태에서만 변경 가능합니다. 토큰 필요"
147+
)
148+
public DataResponse<OrderDetailResponse> updateDeliveryAddress(
149+
@AuthenticationPrincipal Long userId,
150+
@PathVariable Long orderId,
151+
@RequestBody @Valid DeliveryAddressUpdateRequest request
152+
) {
153+
return DataResponse.from(orderService.updateDeliveryAddress(userId, orderId, request));
154+
}
155+
111156
}

src/main/java/com/ongil/backend/domain/order/converter/OrderConverter.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
import com.ongil.backend.domain.order.dto.request.CartOrderRequest;
1414
import com.ongil.backend.domain.order.dto.request.OrderCreateRequest;
1515
import com.ongil.backend.domain.order.dto.request.OrderItemRequest;
16+
import com.ongil.backend.domain.order.dto.response.OrderCancelResponse;
1617
import com.ongil.backend.domain.order.dto.response.OrderDetailResponse;
1718
import com.ongil.backend.domain.order.dto.response.OrderHistoryResponse;
1819
import com.ongil.backend.domain.order.dto.response.OrderItemDto;
1920
import com.ongil.backend.domain.order.dto.response.OrderItemSummaryDto;
2021
import com.ongil.backend.domain.order.dto.response.OrderSummaryDto;
22+
import com.ongil.backend.domain.order.dto.response.RefundInfoDto;
2123
import com.ongil.backend.domain.order.entity.Order;
2224
import com.ongil.backend.domain.order.entity.OrderItem;
2325
import com.ongil.backend.domain.order.enums.OrderStatus;
@@ -67,7 +69,7 @@ public OrderDetailResponse toDetailResponse(Order order, List<OrderItemDto> item
6769
order.getTotalAmount(),
6870
order.getRecipient(),
6971
order.getRecipientPhone(),
70-
order.getDeliveryAddress() + " " + (order.getDetailAddress() != null ? order.getDetailAddress() : ""),
72+
order.getDeliveryAddress() + (order.getDetailAddress() != null ? " " + order.getDetailAddress() : ""),
7173
order.getDeliveryMessage(),
7274
order.getCreatedAt()
7375
);
@@ -96,6 +98,56 @@ public OrderCreateRequest toOrderCreateRequest(List<Cart> cartItems, CartOrderRe
9698
);
9799
}
98100

101+
// OrderItem -> OrderItemDto
102+
public OrderItemDto toOrderItemDto(OrderItem orderItem) {
103+
Product product = orderItem.getProduct();
104+
105+
String imageUrl = "default-image-url";
106+
if (product.getImageUrls() != null && !product.getImageUrls().isBlank()) {
107+
imageUrl = product.getImageUrls().split(",")[0].trim();
108+
}
109+
110+
String brandName = product.getBrand() != null ? product.getBrand().getName() : "일반 브랜드";
111+
112+
return new OrderItemDto(
113+
product.getId(),
114+
brandName,
115+
product.getName(),
116+
imageUrl,
117+
orderItem.getSelectedSize(),
118+
orderItem.getSelectedColor(),
119+
orderItem.getQuantity(),
120+
orderItem.getPriceAtOrder()
121+
);
122+
}
123+
124+
// Order의 OrderItem 리스트 -> OrderItemDto 리스트
125+
public List<OrderItemDto> toOrderItemDtos(Order order) {
126+
return order.getOrderItems().stream()
127+
.map(this::toOrderItemDto)
128+
.toList();
129+
}
130+
131+
// Order -> OrderCancelResponse
132+
public OrderCancelResponse toCancelResponse(Order order, List<OrderItemDto> itemDtos, RefundInfoDto refundInfo) {
133+
return new OrderCancelResponse(
134+
order.getId(),
135+
order.getOrderNumber(),
136+
order.getOrderStatus(),
137+
order.getTotalAmount(),
138+
order.getCanceledAt(),
139+
order.getCancelReason(),
140+
order.getCancelDetail(),
141+
itemDtos,
142+
refundInfo,
143+
order.getDeliveryAddress() + (order.getDetailAddress() != null ? " " + order.getDetailAddress() : ""),
144+
order.getRecipient(),
145+
order.getRecipientPhone(),
146+
order.getDeliveryMessage(),
147+
order.getCreatedAt()
148+
);
149+
}
150+
99151
// Order 엔티티 -> OrderSummaryDto
100152
public OrderSummaryDto toSummaryDto(Order order) {
101153
List<OrderItemSummaryDto> itemDtos = order.getOrderItems().stream()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.ongil.backend.domain.order.dto.request;
2+
3+
import com.ongil.backend.domain.order.enums.CancelReason;
4+
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotNull;
7+
import jakarta.validation.constraints.Size;
8+
9+
@Schema(description = "환불 정보 조회 요청")
10+
public record CancelRefundInfoRequest(
11+
12+
@Schema(description = "취소 사유")
13+
@NotNull(message = "취소 사유는 필수입니다")
14+
CancelReason cancelReason,
15+
16+
@Schema(description = "상세 취소 사유 (1~300자)")
17+
@Size(min = 1, max = 300, message = "상세 사유는 1자 이상 300자 이하로 입력해주세요")
18+
String cancelDetail
19+
) {
20+
}

0 commit comments

Comments
 (0)